Refactor AWG to Clash conversion script; enhance option normalization, parsing logic, and proxy construction for improved functionality and maintainability
This commit is contained in:
@@ -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")
|
.replace(/\r\n/g, "\n")
|
||||||
.split("\n")
|
.split("\n");
|
||||||
.map(l => l.trim())
|
}
|
||||||
.filter(l => l && !l.startsWith("#") && !l.startsWith(";"));
|
|
||||||
|
// Парсим один 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;
|
let section = null;
|
||||||
const data = { Interface: {}, Peer: {} };
|
const data = { Interface: {}, Peer: {} };
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const sec = line.match(/^\[(.+?)\]$/);
|
const mSec = line.match(/^\[(.+?)\]$/);
|
||||||
if (sec) {
|
if (mSec) {
|
||||||
section = sec[1];
|
section = mSec[1];
|
||||||
continue;
|
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;
|
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(",")
|
.split(",")
|
||||||
.map(x => x.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scalar(v) {
|
function parseEndpoint(endpoint) {
|
||||||
if (typeof v === "number") return v;
|
// Поддержка:
|
||||||
if (typeof v === "boolean") return v;
|
// - host:port
|
||||||
return String(v);
|
// - [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);
|
function toNumberOrNull(v) {
|
||||||
const i = wg.Interface;
|
const s = String(v ?? "").trim();
|
||||||
const p = wg.Peer;
|
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 = {
|
for (const K of keys) {
|
||||||
name: "amz-wg",
|
const fromFile = interfaceObj[K];
|
||||||
type: "wireguard",
|
const fileNum = toNumberOrNull(fromFile);
|
||||||
ip: i.Address,
|
const fallback = AMZ_DEFAULTS[K];
|
||||||
"private-key": i.PrivateKey,
|
|
||||||
peers: [{
|
const finalVal =
|
||||||
server,
|
fileNum !== null ? fileNum : (fallback ?? 0);
|
||||||
port: Number(port),
|
|
||||||
"public-key": p.PublicKey,
|
if (Number(finalVal) !== 0) {
|
||||||
"pre-shared-key": p.PresharedKey,
|
out[K.toLowerCase()] = Number(finalVal);
|
||||||
"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)
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// 🔴 ВАЖНО: перезаписываем $content
|
function filterAllowedIPs(allowed, enableIPv6) {
|
||||||
$content = ProxyUtils.yaml.safeDump({
|
if (enableIPv6) return allowed;
|
||||||
proxies: [proxy]
|
// выкидываем всё, что похоже на 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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user