Refactor AWG to Clash conversion script for improved readability and maintainability; streamline parsing and output generation

This commit is contained in:
2026-01-05 18:34:33 +03:00
parent 5cef2d7bca
commit e8477c3322

View File

@@ -1,176 +1,96 @@
/**
* 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) { function parseIniWG(text) {
const lines = text const lines = text
.replace(/\r\n/g, "\n") .replace(/\r\n/g, "\n")
.split("\n") .split("\n")
.map((l) => l.trim()) .map(l => l.trim())
.filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith(";")); .filter(l => l && !l.startsWith("#") && !l.startsWith(";"));
let section = null; let section = null;
const data = { Interface: {}, Peer: {} }; const data = { Interface: {}, Peer: {} };
for (const line of lines) { for (const line of lines) {
const mSec = line.match(/^\[(.+?)\]$/); const sec = line.match(/^\[(.+?)\]$/);
if (mSec) { if (sec) {
section = mSec[1]; section = sec[1];
continue; continue;
} }
const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/);
if (!mKV || !section) continue;
const key = mKV[1].trim(); const kv = line.match(/^([^=]+?)\s*=\s*(.+)$/);
const value = mKV[2].trim(); if (!kv || !section) continue;
if (section.toLowerCase() === "interface") data.Interface[key] = value; const key = kv[1].trim();
else if (section.toLowerCase() === "peer") data.Peer[key] = value; const val = kv[2].trim();
if (section === "Interface") data.Interface[key] = val;
if (section === "Peer") data.Peer[key] = val;
} }
return data; return data;
} }
function splitList(val) { function splitList(v) {
// "a, b, c" -> ["a","b","c"] return String(v || "")
return String(val || "")
.split(",") .split(",")
.map((s) => s.trim()) .map(x => x.trim())
.filter(Boolean); .filter(Boolean);
} }
function parseEndpoint(endpoint) { function parseEndpoint(ep) {
// supports: host:port (IPv6 in brackets not handled here; can add if needed) const m = String(ep || "").match(/^(.+?):(\d+)$/);
const m = String(endpoint || "").match(/^(.+?):(\d+)$/);
if (!m) return { host: "", port: 0 }; if (!m) return { host: "", port: 0 };
return { host: m[1], port: Number(m[2]) }; return { host: m[1], port: Number(m[2]) };
} }
function toNumIfPossible(v) { function scalar(v) {
const s = String(v ?? "").trim(); if (typeof v === "number") return String(v);
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"; if (typeof v === "boolean") return v ? "true" : "false";
return yamlEscapeString(v); return `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
} }
function yamlInlineArray(arr) { function arrayInline(arr) {
const a = Array.isArray(arr) ? arr : []; return `[${arr.map(scalar).join(", ")}]`;
return `[${a.map((x) => yamlScalar(x)).join(", ")}]`;
} }
function buildClashProxyFromWG(wg, proxyName) { function main() {
const i = wg.Interface || {}; const input = $content; // Sub Store magic variable
const p = wg.Peer || {};
const address = i.Address || ""; const wg = parseIniWG(input);
const dnsList = splitList(i.DNS); const i = wg.Interface;
const p = wg.Peer;
const { host, port } = parseEndpoint(p.Endpoint);
const dns = splitList(i.DNS);
const allowed = splitList(p.AllowedIPs); const allowed = splitList(p.AllowedIPs);
const ep = parseEndpoint(p.Endpoint);
// Amnezia fields (case-insensitive keys are NOT guaranteed in input, handle both) const amz = {};
const pick = (k) => (i[k] !== undefined ? i[k] : i[k.toLowerCase()] !== undefined ? i[k.toLowerCase()] : undefined); ["Jc","Jmin","Jmax","S1","S2","H1","H2","H3","H4"].forEach(k => {
if (i[k] !== undefined) amz[k.toLowerCase()] = Number(i[k]);
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 = ""; let out = "";
out += "proxies:\n"; out += "proxies:\n";
out += ` - name: ${yamlEscapeString(name)}\n`; out += " - name: \"amz-wg\"\n";
out += ` type: wireguard\n`; out += " type: wireguard\n";
out += ` ip: ${yamlScalar(address)}\n`; out += ` ip: ${scalar(i.Address)}\n`;
out += ` private-key: ${yamlScalar(i.PrivateKey || "")}\n`; out += ` private-key: ${scalar(i.PrivateKey)}\n`;
out += ` peers:\n`; out += " peers:\n";
out += ` - server: ${yamlScalar(host)}\n`; out += ` - server: ${scalar(ep.host)}\n`;
out += ` port: ${yamlScalar(port)}\n`; out += ` port: ${ep.port}\n`;
out += ` public-key: ${yamlScalar(p.PublicKey || "")}\n`; out += ` public-key: ${scalar(p.PublicKey)}\n`;
if (p.PresharedKey) out += ` pre-shared-key: ${yamlScalar(p.PresharedKey)}\n`; if (p.PresharedKey)
out += ` allowed-ips: ${yamlInlineArray(allowed)}\n`; out += ` pre-shared-key: ${scalar(p.PresharedKey)}\n`;
out += ` udp: true\n`; out += ` allowed-ips: ${arrayInline(allowed)}\n`;
out += ` remote-dns-resolve: true\n`; out += " udp: true\n";
if (dnsList.length) out += ` dns: ${yamlInlineArray(dnsList)}\n`; out += " remote-dns-resolve: true\n";
if (Object.keys(amzObj).length) { if (dns.length) out += ` dns: ${arrayInline(dns)}\n`;
out += ` amnezia-wg-option:\n`;
for (const k of Object.keys(amzObj)) { if (Object.keys(amz).length) {
out += ` ${k}: ${yamlScalar(amzObj[k])}\n`; out += " amnezia-wg-option:\n";
for (const k in amz) {
out += ` ${k}: ${amz[k]}\n`;
} }
} }
return out; return out;
} }
/** main();
* 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;
};