Files
clash-rules/config-sub-converter/scripts/convert-awg-to-clash.js

177 lines
4.8 KiB
JavaScript

/**
* Sub Store JS script: AmneziaWG/ProtonVPN (WG INI) -> Clash (wireguard proxy)
*
* Expected input: text like:
* [Interface]
* Address = ...
* DNS = ...
* PrivateKey = ...
* ...
* [Peer]
* PublicKey = ...
* PresharedKey = ...
* AllowedIPs = ...
* Endpoint = host:port
*
* Output: YAML fragment:
* proxies:
* - name: "..."
* type: wireguard
* ...
*/
function parseIniWG(text) {
const lines = text
.replace(/\r\n/g, "\n")
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith(";"));
let section = null;
const data = { Interface: {}, Peer: {} };
for (const line of lines) {
const mSec = line.match(/^\[(.+?)\]$/);
if (mSec) {
section = mSec[1];
continue;
}
const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/);
if (!mKV || !section) continue;
const key = mKV[1].trim();
const value = mKV[2].trim();
if (section.toLowerCase() === "interface") data.Interface[key] = value;
else if (section.toLowerCase() === "peer") data.Peer[key] = value;
}
return data;
}
function splitList(val) {
// "a, b, c" -> ["a","b","c"]
return String(val || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
function parseEndpoint(endpoint) {
// supports: host:port (IPv6 in brackets not handled here; can add if needed)
const m = String(endpoint || "").match(/^(.+?):(\d+)$/);
if (!m) return { host: "", port: 0 };
return { host: m[1], port: Number(m[2]) };
}
function toNumIfPossible(v) {
const s = String(v ?? "").trim();
if (s === "") return null;
if (/^-?\d+$/.test(s)) return Number(s);
return s;
}
function yamlEscapeString(s) {
// simplest safe quoting
const str = String(s ?? "");
return `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}
function yamlScalar(v) {
if (v === null || v === undefined) return "null";
if (typeof v === "number" && Number.isFinite(v)) return String(v);
if (typeof v === "boolean") return v ? "true" : "false";
return yamlEscapeString(v);
}
function yamlInlineArray(arr) {
const a = Array.isArray(arr) ? arr : [];
return `[${a.map((x) => yamlScalar(x)).join(", ")}]`;
}
function buildClashProxyFromWG(wg, proxyName) {
const i = wg.Interface || {};
const p = wg.Peer || {};
const address = i.Address || "";
const dnsList = splitList(i.DNS);
const { host, port } = parseEndpoint(p.Endpoint);
const allowed = splitList(p.AllowedIPs);
// Amnezia fields (case-insensitive keys are NOT guaranteed in input, handle both)
const pick = (k) => (i[k] !== undefined ? i[k] : i[k.toLowerCase()] !== undefined ? i[k.toLowerCase()] : undefined);
const amzKeys = [
["jc", pick("Jc")],
["jmin", pick("Jmin")],
["jmax", pick("Jmax")],
["s1", pick("S1")],
["s2", pick("S2")],
["h1", pick("H1")],
["h2", pick("H2")],
["h3", pick("H3")],
["h4", pick("H4")],
];
const amzObj = {};
for (const [k, v] of amzKeys) {
if (v !== undefined) amzObj[k] = toNumIfPossible(v);
}
// Compose YAML (manual, predictable formatting)
const name = proxyName && String(proxyName).trim() ? String(proxyName).trim() : "amz-wg";
let out = "";
out += "proxies:\n";
out += ` - name: ${yamlEscapeString(name)}\n`;
out += ` type: wireguard\n`;
out += ` ip: ${yamlScalar(address)}\n`;
out += ` private-key: ${yamlScalar(i.PrivateKey || "")}\n`;
out += ` peers:\n`;
out += ` - server: ${yamlScalar(host)}\n`;
out += ` port: ${yamlScalar(port)}\n`;
out += ` public-key: ${yamlScalar(p.PublicKey || "")}\n`;
if (p.PresharedKey) out += ` pre-shared-key: ${yamlScalar(p.PresharedKey)}\n`;
out += ` allowed-ips: ${yamlInlineArray(allowed)}\n`;
out += ` udp: true\n`;
out += ` remote-dns-resolve: true\n`;
if (dnsList.length) out += ` dns: ${yamlInlineArray(dnsList)}\n`;
if (Object.keys(amzObj).length) {
out += ` amnezia-wg-option:\n`;
for (const k of Object.keys(amzObj)) {
out += ` ${k}: ${yamlScalar(amzObj[k])}\n`;
}
}
return out;
}
/**
* Sub Store entrypoint.
* Most Sub Store scripts expose a function like:
* module.exports = async (ctx) => { ... }
* where ctx has raw subscription text in ctx.content / ctx.body / etc.
*
* To be robust across environments, we try common fields.
*/
module.exports = async (ctx) => {
const input =
(ctx && (ctx.content || ctx.body || ctx.data || ctx.text)) ??
"";
const wg = parseIniWG(String(input));
// Try to derive a name if Sub Store provides something (optional)
// Fallback: "amz-wg"
const derivedName =
(ctx && (ctx.name || ctx.profileName || ctx.filename)) ||
"amz-wg";
const yaml = buildClashProxyFromWG(wg, derivedName);
// Return in whatever field Sub Store expects
// Common patterns: return string, or set ctx.content
if (ctx) ctx.content = yaml;
return yaml;
};