diff --git a/config-sub-converter/scripts/convert-awg-to-clash.js b/config-sub-converter/scripts/convert-awg-to-clash.js index 9503ddd..4cd67c7 100644 --- a/config-sub-converter/scripts/convert-awg-to-clash.js +++ b/config-sub-converter/scripts/convert-awg-to-clash.js @@ -1,75 +1,266 @@ -function parseIniWG(text) { - const lines = text +/********************** + * Defaults (AmneziaWG) + * Если в исходнике нет параметра, берём отсюда. + * Если итоговое значение == 0, параметр пропускаем в amnezia-wg-option. + **********************/ +const AMZ_DEFAULTS = { + Jc: 3, + Jmin: 10, + Jmax: 50, + S1: 0, + S2: 0, + H1: 0, + H2: 0, + H3: 0, + H4: 0, +}; + +/********************** + * Options from Sub Store + * Example URL: + * .../convert-awg-to-clash.js#dns=false&ipv6=false#noCache + * + * Требования: + * - dns=false => remote-dns-resolve: false (вне зависимости от входа) + * - ipv6=false => удалить IPv6 из allowed-ips (и вообще не добавлять ipv6-части) + **********************/ +function normalizeOptions(rawOptions) { + // $options может быть объектом или строкой или чем-то странным. + const opts = rawOptions ?? {}; + let q = ""; + + if (typeof opts === "string") { + q = opts; + } else if (typeof opts === "object") { + // часто движки кладут query в одном из полей + q = + opts.query || + opts.params || + opts.search || + opts.hash || + opts.raw || + ""; + } + + // Если q выглядит как "dns=false&ipv6=false" + const params = {}; + if (typeof q === "string" && q.includes("=")) { + for (const part of q.split("&")) { + const [k, v] = part.split("="); + if (!k) continue; + params[String(k).trim()] = (v ?? "").trim(); + } + } + + // Фоллбек: если $options уже содержит ключи dns/ipv6 + const get = (key) => { + if (params[key] !== undefined) return params[key]; + if (opts[key] !== undefined) return opts[key]; + return undefined; + }; + + const asBool = (v, defaultVal = true) => { + if (v === undefined || v === null || v === "") return defaultVal; + if (typeof v === "boolean") return v; + const s = String(v).toLowerCase().trim(); + if (["1", "true", "yes", "y", "on"].includes(s)) return true; + if (["0", "false", "no", "n", "off"].includes(s)) return false; + return defaultVal; + }; + + return { + dns: asBool(get("dns"), true), + ipv6: asBool(get("ipv6"), true), + }; +} + +/********************** + * Parsing WG INI blocks + **********************/ +function cleanLines(text) { + return String(text ?? "") .replace(/\r\n/g, "\n") - .split("\n") - .map(l => l.trim()) - .filter(l => l && !l.startsWith("#") && !l.startsWith(";")); + .split("\n"); +} + +// Парсим один INI-фрагмент с [Interface] и [Peer] (один peer) +function parseIniOne(text) { + const lines = cleanLines(text) + .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 sec = line.match(/^\[(.+?)\]$/); - if (sec) { - section = sec[1]; + const mSec = line.match(/^\[(.+?)\]$/); + if (mSec) { + section = mSec[1]; continue; } - const kv = line.match(/^([^=]+?)\s*=\s*(.+)$/); - if (!kv || !section) continue; - data[section][kv[1].trim()] = kv[2].trim(); + const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/); + if (!mKV || !section) continue; + + const key = mKV[1].trim(); + const value = mKV[2].trim(); + + if (section === "Interface") data.Interface[key] = value; + else if (section === "Peer") data.Peer[key] = value; } + return data; } -function split(v) { - return String(v || "") +// Разбиваем весь файл на блоки по заголовкам "##### ..." +function splitByHeaders(fullText) { + const lines = cleanLines(fullText); + + const blocks = []; + let current = { name: "amz-wg", buf: [] }; + + const headerRe = /^#{5}\s*(.+)\s*$/; + + for (const line of lines) { + const mh = line.match(headerRe); + if (mh) { + // закрываем предыдущий блок, если там что-то есть + if (current.buf.join("\n").trim().length > 0) blocks.push(current); + current = { name: mh[1].trim(), buf: [] }; + continue; + } + current.buf.push(line); + } + + if (current.buf.join("\n").trim().length > 0) blocks.push(current); + + return blocks; +} + +function splitList(val) { + return String(val || "") .split(",") - .map(x => x.trim()) + .map((s) => s.trim()) .filter(Boolean); } -function scalar(v) { - if (typeof v === "number") return v; - if (typeof v === "boolean") return v; - return String(v); +function parseEndpoint(endpoint) { + // Поддержка: + // - host:port + // - [ipv6]:port + const s = String(endpoint || "").trim(); + const v6 = s.match(/^\[(.+?)\]:(\d+)$/); + if (v6) return { host: v6[1], port: Number(v6[2]) }; + + const v4 = s.match(/^(.+?):(\d+)$/); + if (v4) return { host: v4[1], port: Number(v4[2]) }; + + return { host: "", port: 0 }; } -const wg = parseIniWG($content); -const i = wg.Interface; -const p = wg.Peer; +function toNumberOrNull(v) { + const s = String(v ?? "").trim(); + if (s === "") return null; + if (/^-?\d+$/.test(s)) return Number(s); + return null; +} -const [server, port] = p.Endpoint.split(":"); +function buildAmzOptions(interfaceObj) { + // Правило: + // - если в файле есть параметр => используем его + // - иначе берём из AMZ_DEFAULTS + // - если итог == 0 => пропускаем + const out = {}; + const keys = Object.keys(AMZ_DEFAULTS); -const proxy = { - name: "amz-wg", - type: "wireguard", - ip: i.Address, - "private-key": i.PrivateKey, - peers: [{ - server, - port: Number(port), - "public-key": p.PublicKey, - "pre-shared-key": p.PresharedKey, - "allowed-ips": split(p.AllowedIPs) - }], - udp: true, - "remote-dns-resolve": true, - dns: split(i.DNS), - "amnezia-wg-option": { - jc: Number(i.Jc), - jmin: Number(i.Jmin), - jmax: Number(i.Jmax), - s1: Number(i.S1), - s2: Number(i.S2), - h1: Number(i.H1), - h2: Number(i.H2), - h3: Number(i.H3), - h4: Number(i.H4) + for (const K of keys) { + const fromFile = interfaceObj[K]; + const fileNum = toNumberOrNull(fromFile); + const fallback = AMZ_DEFAULTS[K]; + + const finalVal = + fileNum !== null ? fileNum : (fallback ?? 0); + + if (Number(finalVal) !== 0) { + out[K.toLowerCase()] = Number(finalVal); + } } -}; + return out; +} -// 🔴 ВАЖНО: перезаписываем $content -$content = ProxyUtils.yaml.safeDump({ - proxies: [proxy] -}); +function filterAllowedIPs(allowed, enableIPv6) { + if (enableIPv6) return allowed; + // выкидываем всё, что похоже на IPv6 + return allowed.filter((cidr) => !cidr.includes(":")); +} + +function buildProxy(blockName, wg, options) { + const i = wg.Interface || {}; + const p = wg.Peer || {}; + + const address = i.Address || ""; + const dnsList = splitList(i.DNS); + + const ep = parseEndpoint(p.Endpoint); + let allowed = splitList(p.AllowedIPs); + allowed = filterAllowedIPs(allowed, options.ipv6); + + const proxy = { + name: blockName || "amz-wg", + type: "wireguard", + ip: address, + // ipv6 поле в твоём примере закомментировано, так что не добавляем вообще + "private-key": i.PrivateKey || "", + peers: [ + { + server: ep.host, + port: ep.port, + "public-key": p.PublicKey || "", + ...(p.PresharedKey ? { "pre-shared-key": p.PresharedKey } : {}), + "allowed-ips": allowed, + }, + ], + udp: true, + // dns=false => принудительно false + "remote-dns-resolve": options.dns ? true : false, + ...(dnsList.length ? { dns: dnsList } : {}), + }; + + const amz = buildAmzOptions(i); + if (Object.keys(amz).length) { + proxy["amnezia-wg-option"] = amz; + } + + return proxy; +} + +/********************** + * ENTRYPOINT + **********************/ +const opts = normalizeOptions($options); + +// Вход: чаще всего $content, но на всякий пожарный берём $files[0] +const input = String($content ?? ($files && $files[0]) ?? ""); + +// Разбиваем по заголовкам ##### ... +const blocks = splitByHeaders(input); + +// Для каждого блока парсим INI и строим proxy +const proxies = []; +for (const b of blocks) { + const iniText = b.buf.join("\n").trim(); + if (!iniText) continue; + + const wg = parseIniOne(iniText); + + // минимальная валидация: нужны ключи + if (!wg.Interface?.PrivateKey || !wg.Peer?.PublicKey || !wg.Peer?.Endpoint) { + // пропускаем мусорные блоки, чтобы не ронять весь конвертер + continue; + } + + proxies.push(buildProxy(b.name, wg, opts)); +} + +// Финальный YAML +$content = ProxyUtils.yaml.safeDump({ proxies });