From e8477c3322bcf779031e398770cee230d02c3b44 Mon Sep 17 00:00:00 2001 From: DaTekShaman Date: Mon, 5 Jan 2026 18:34:33 +0300 Subject: [PATCH] Refactor AWG to Clash conversion script for improved readability and maintainability; streamline parsing and output generation --- .../scripts/convert-awg-to-clash.js | 186 +++++------------- 1 file changed, 53 insertions(+), 133 deletions(-) diff --git a/config-sub-converter/scripts/convert-awg-to-clash.js b/config-sub-converter/scripts/convert-awg-to-clash.js index 1fefc8a..f82c1ff 100644 --- a/config-sub-converter/scripts/convert-awg-to-clash.js +++ b/config-sub-converter/scripts/convert-awg-to-clash.js @@ -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) { const lines = text .replace(/\r\n/g, "\n") .split("\n") - .map((l) => l.trim()) - .filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith(";")); + .map(l => l.trim()) + .filter(l => l && !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]; + const sec = line.match(/^\[(.+?)\]$/); + if (sec) { + section = sec[1]; continue; } - const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/); - if (!mKV || !section) continue; - const key = mKV[1].trim(); - const value = mKV[2].trim(); + const kv = line.match(/^([^=]+?)\s*=\s*(.+)$/); + if (!kv || !section) continue; - if (section.toLowerCase() === "interface") data.Interface[key] = value; - else if (section.toLowerCase() === "peer") data.Peer[key] = value; + const key = kv[1].trim(); + const val = kv[2].trim(); + + if (section === "Interface") data.Interface[key] = val; + if (section === "Peer") data.Peer[key] = val; } return data; } -function splitList(val) { - // "a, b, c" -> ["a","b","c"] - return String(val || "") +function splitList(v) { + return String(v || "") .split(",") - .map((s) => s.trim()) + .map(x => x.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+)$/); +function parseEndpoint(ep) { + const m = String(ep || "").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); +function scalar(v) { + if (typeof v === "number") return String(v); if (typeof v === "boolean") return v ? "true" : "false"; - return yamlEscapeString(v); + return `"${String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; } -function yamlInlineArray(arr) { - const a = Array.isArray(arr) ? arr : []; - return `[${a.map((x) => yamlScalar(x)).join(", ")}]`; +function arrayInline(arr) { + return `[${arr.map(scalar).join(", ")}]`; } -function buildClashProxyFromWG(wg, proxyName) { - const i = wg.Interface || {}; - const p = wg.Peer || {}; +function main() { + const input = $content; // Sub Store magic variable - const address = i.Address || ""; - const dnsList = splitList(i.DNS); - - const { host, port } = parseEndpoint(p.Endpoint); + const wg = parseIniWG(input); + const i = wg.Interface; + const p = wg.Peer; + const dns = splitList(i.DNS); const allowed = splitList(p.AllowedIPs); + const ep = parseEndpoint(p.Endpoint); - // 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"; + const amz = {}; + ["Jc","Jmin","Jmax","S1","S2","H1","H2","H3","H4"].forEach(k => { + if (i[k] !== undefined) amz[k.toLowerCase()] = Number(i[k]); + }); 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`; + out += " - name: \"amz-wg\"\n"; + out += " type: wireguard\n"; + out += ` ip: ${scalar(i.Address)}\n`; + out += ` private-key: ${scalar(i.PrivateKey)}\n`; + out += " peers:\n"; + out += ` - server: ${scalar(ep.host)}\n`; + out += ` port: ${ep.port}\n`; + out += ` public-key: ${scalar(p.PublicKey)}\n`; + if (p.PresharedKey) + out += ` pre-shared-key: ${scalar(p.PresharedKey)}\n`; + out += ` allowed-ips: ${arrayInline(allowed)}\n`; + out += " udp: true\n"; + out += " remote-dns-resolve: true\n"; + if (dns.length) out += ` dns: ${arrayInline(dns)}\n`; + + if (Object.keys(amz).length) { + out += " amnezia-wg-option:\n"; + for (const k in amz) { + out += ` ${k}: ${amz[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; -}; +main();