254 lines
6.8 KiB
JavaScript
254 lines
6.8 KiB
JavaScript
/**********************
|
||
* 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 });
|