177 lines
4.8 KiB
JavaScript
177 lines
4.8 KiB
JavaScript
/**
|
|
* Sub Store JS script: AmneziaWG/ProtonVPN (WG INI) -> Clash (wireguard proxy)
|
|
*
|
|
* Expected input: text like:
|
|
* [Interface]
|
|
* Address = ...
|
|
* DNS = ...
|
|
* PrivateKey = ...
|
|
* ...
|
|
* [Peer]
|
|
* PublicKey = ...
|
|
* PresharedKey = ...
|
|
* AllowedIPs = ...
|
|
* Endpoint = host:port
|
|
*
|
|
* Output: YAML fragment:
|
|
* proxies:
|
|
* - name: "..."
|
|
* type: wireguard
|
|
* ...
|
|
*/
|
|
function parseIniWG(text) {
|
|
const lines = text
|
|
.replace(/\r\n/g, "\n")
|
|
.split("\n")
|
|
.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.toLowerCase() === "interface") data.Interface[key] = value;
|
|
else if (section.toLowerCase() === "peer") data.Peer[key] = value;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function splitList(val) {
|
|
// "a, b, c" -> ["a","b","c"]
|
|
return String(val || "")
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function parseEndpoint(endpoint) {
|
|
// supports: host:port (IPv6 in brackets not handled here; can add if needed)
|
|
const m = String(endpoint || "").match(/^(.+?):(\d+)$/);
|
|
if (!m) return { host: "", port: 0 };
|
|
return { host: m[1], port: Number(m[2]) };
|
|
}
|
|
|
|
function toNumIfPossible(v) {
|
|
const s = String(v ?? "").trim();
|
|
if (s === "") return null;
|
|
if (/^-?\d+$/.test(s)) return Number(s);
|
|
return s;
|
|
}
|
|
|
|
function yamlEscapeString(s) {
|
|
// simplest safe quoting
|
|
const str = String(s ?? "");
|
|
return `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
}
|
|
|
|
function yamlScalar(v) {
|
|
if (v === null || v === undefined) return "null";
|
|
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
if (typeof v === "boolean") return v ? "true" : "false";
|
|
return yamlEscapeString(v);
|
|
}
|
|
|
|
function yamlInlineArray(arr) {
|
|
const a = Array.isArray(arr) ? arr : [];
|
|
return `[${a.map((x) => yamlScalar(x)).join(", ")}]`;
|
|
}
|
|
|
|
function buildClashProxyFromWG(wg, proxyName) {
|
|
const i = wg.Interface || {};
|
|
const p = wg.Peer || {};
|
|
|
|
const address = i.Address || "";
|
|
const dnsList = splitList(i.DNS);
|
|
|
|
const { host, port } = parseEndpoint(p.Endpoint);
|
|
|
|
const allowed = splitList(p.AllowedIPs);
|
|
|
|
// Amnezia fields (case-insensitive keys are NOT guaranteed in input, handle both)
|
|
const pick = (k) => (i[k] !== undefined ? i[k] : i[k.toLowerCase()] !== undefined ? i[k.toLowerCase()] : undefined);
|
|
|
|
const amzKeys = [
|
|
["jc", pick("Jc")],
|
|
["jmin", pick("Jmin")],
|
|
["jmax", pick("Jmax")],
|
|
["s1", pick("S1")],
|
|
["s2", pick("S2")],
|
|
["h1", pick("H1")],
|
|
["h2", pick("H2")],
|
|
["h3", pick("H3")],
|
|
["h4", pick("H4")],
|
|
];
|
|
|
|
const amzObj = {};
|
|
for (const [k, v] of amzKeys) {
|
|
if (v !== undefined) amzObj[k] = toNumIfPossible(v);
|
|
}
|
|
|
|
// Compose YAML (manual, predictable formatting)
|
|
const name = proxyName && String(proxyName).trim() ? String(proxyName).trim() : "amz-wg";
|
|
|
|
let out = "";
|
|
out += "proxies:\n";
|
|
out += ` - name: ${yamlEscapeString(name)}\n`;
|
|
out += ` type: wireguard\n`;
|
|
out += ` ip: ${yamlScalar(address)}\n`;
|
|
out += ` private-key: ${yamlScalar(i.PrivateKey || "")}\n`;
|
|
out += ` peers:\n`;
|
|
out += ` - server: ${yamlScalar(host)}\n`;
|
|
out += ` port: ${yamlScalar(port)}\n`;
|
|
out += ` public-key: ${yamlScalar(p.PublicKey || "")}\n`;
|
|
if (p.PresharedKey) out += ` pre-shared-key: ${yamlScalar(p.PresharedKey)}\n`;
|
|
out += ` allowed-ips: ${yamlInlineArray(allowed)}\n`;
|
|
out += ` udp: true\n`;
|
|
out += ` remote-dns-resolve: true\n`;
|
|
if (dnsList.length) out += ` dns: ${yamlInlineArray(dnsList)}\n`;
|
|
if (Object.keys(amzObj).length) {
|
|
out += ` amnezia-wg-option:\n`;
|
|
for (const k of Object.keys(amzObj)) {
|
|
out += ` ${k}: ${yamlScalar(amzObj[k])}\n`;
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Sub Store entrypoint.
|
|
* Most Sub Store scripts expose a function like:
|
|
* module.exports = async (ctx) => { ... }
|
|
* where ctx has raw subscription text in ctx.content / ctx.body / etc.
|
|
*
|
|
* To be robust across environments, we try common fields.
|
|
*/
|
|
module.exports = async (ctx) => {
|
|
const input =
|
|
(ctx && (ctx.content || ctx.body || ctx.data || ctx.text)) ??
|
|
"";
|
|
|
|
const wg = parseIniWG(String(input));
|
|
|
|
// Try to derive a name if Sub Store provides something (optional)
|
|
// Fallback: "amz-wg"
|
|
const derivedName =
|
|
(ctx && (ctx.name || ctx.profileName || ctx.filename)) ||
|
|
"amz-wg";
|
|
|
|
const yaml = buildClashProxyFromWG(wg, derivedName);
|
|
|
|
// Return in whatever field Sub Store expects
|
|
// Common patterns: return string, or set ctx.content
|
|
if (ctx) ctx.content = yaml;
|
|
return yaml;
|
|
};
|