Refactor AWG to Clash conversion script; enhance option normalization, parsing logic, and proxy construction for improved functionality and maintainability

This commit is contained in:
2026-01-05 19:37:34 +03:00
parent 26867d4175
commit ba21e17168

View File

@@ -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 });