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")
|
||||
.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 });
|
||||
|
||||
Reference in New Issue
Block a user