/********************** * 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) { // rawOptions может быть // 1) {} // 2) { meta: { url: "..." } } // 3) { url: "..." } const urlStr = (rawOptions && rawOptions.meta && rawOptions.meta.url) || rawOptions.url || ""; // Ищем part после # const hashIdx = urlStr.indexOf("#"); const qstr = hashIdx >= 0 ? urlStr.slice(hashIdx + 1) : ""; const params = {}; for (const part of qstr.split("&")) { const [k, v] = part.split("="); if (!k) continue; params[k.trim().toLowerCase()] = (v ?? "").trim(); } const asBool = (v, def) => { if (v === undefined || v === "") return def; const s = String(v).toLowerCase(); if (["1","true","yes","on"].includes(s)) return true; if (["0","false","no","off"].includes(s)) return false; return def; }; return { dns: asBool(params.dns, true), ipv6: asBool(params.ipv6, true), }; } /********************** * Parsing WG INI blocks **********************/ function cleanLines(text) { return String(text ?? "") .replace(/\r\n/g, "\n") .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 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 === "Interface") data.Interface[key] = value; else if (section === "Peer") data.Peer[key] = value; } return data; } // Разбиваем весь файл на блоки по заголовкам "##### ..." 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((s) => s.trim()) .filter(Boolean); } 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 }; } function toNumberOrNull(v) { const s = String(v ?? "").trim(); if (s === "") return null; if (/^-?\d+$/.test(s)) return Number(s); return null; } function buildAmzOptions(interfaceObj) { // Правило: // - если в файле есть параметр => используем его // - иначе берём из AMZ_DEFAULTS // - если итог == 0 => пропускаем const out = {}; const keys = Object.keys(AMZ_DEFAULTS); 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; } 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 });