mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-04-21 05:09:32 +08:00
3179 lines
119 KiB
JavaScript
3179 lines
119 KiB
JavaScript
/*
|
|
/$$$$$$ /$$ /$$$$$$ /$$
|
|
/$$__ $$ | $$ /$$__ $$| $$
|
|
| $$ \__//$$ /$| $$$$$$$ | $$ \__/$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$
|
|
| $$$$$$| $$ | $| $$__ $$/$$$$$| $$$$$|_ $$_/ /$$__ $$/$$__ $$/$$__ $$
|
|
\____ $| $$ | $| $$ \ $|______/\____ $$| $$ | $$ \ $| $$ \__| $$$$$$$$
|
|
/$$ \ $| $$ | $| $$ | $$ /$$ \ $$| $$ /$| $$ | $| $$ | $$_____/
|
|
| $$$$$$| $$$$$$| $$$$$$$/ | $$$$$$/| $$$$| $$$$$$| $$ | $$$$$$$
|
|
\______/ \______/|_______/ \______/ \___/ \______/|__/ \_______/
|
|
|
|
Sub-Store 资源解析器版 © Peng-YM
|
|
@author: Peng-YM
|
|
@github: https://github.com/Peng-YM/Sub-Store
|
|
*/
|
|
const $ = API("sub-store");
|
|
const Base64 = new Base64Code();
|
|
|
|
function parseResource() {
|
|
// parse
|
|
let result = $resource;
|
|
switch ($resourceType) {
|
|
case 1:
|
|
const proxies = ProxyUtils.parse($resource);
|
|
result = ProxyUtils.produce(proxies, "Loon");
|
|
break;
|
|
case 2:
|
|
const rules = RuleUtils.parse($resource);
|
|
result = RuleUtils.produce(rules, "Loon");
|
|
break;
|
|
}
|
|
$done(result);
|
|
}
|
|
|
|
/****************************************** Proxy Utils **********************************************************/
|
|
var ProxyUtils = (function () {
|
|
const PROXY_PREPROCESSORS = (function () {
|
|
function HTML() {
|
|
const name = "HTML";
|
|
const test = raw => /^<!DOCTYPE html>/.test(raw);
|
|
// simply discard HTML
|
|
const parse = _ => "";
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Base64Encoded() {
|
|
const name = "Base64 Pre-processor";
|
|
|
|
const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v",
|
|
"c2hhZG93", "aHR0c"
|
|
];
|
|
|
|
const test = function (raw) {
|
|
return keys.some(k => raw.indexOf(k) !== -1);
|
|
}
|
|
const parse = function (raw) {
|
|
raw = Base64.safeDecode(raw);
|
|
return raw;
|
|
}
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Clash() {
|
|
const name = "Clash Pre-processor";
|
|
const test = function (raw) {
|
|
return /proxies/.test(raw);
|
|
}
|
|
const parse = function (raw) {
|
|
// Clash YAML format
|
|
// codes are modified from @KOP-XIAO
|
|
// https://github.com/KOP-XIAO/QuantumultX
|
|
if (raw.indexOf("{") !== -1) {
|
|
raw = raw
|
|
.replace(/ - /g, " - ")
|
|
.replace(/:(?!\s)/g, ": ")
|
|
.replace(/\,\"/g, ', "')
|
|
.replace(/: {/g, ": {, ")
|
|
.replace(/, (\"?host|path|tls|mux|skip\"?)/g, ", $1")
|
|
.replace(/{name: /g, '{name: "')
|
|
.replace(/, server:/g, '", server:')
|
|
.replace(/{|}/g, "")
|
|
.replace(/,/g, "\n ");
|
|
}
|
|
raw = raw.replace(/ -\n.*name/g, " - name")
|
|
.replace(/\$|\`/g, "")
|
|
.split("proxy-providers:")[0]
|
|
.split("proxy-groups:")[0]
|
|
.replace(/\"([\w-]+)\"\s*:/g, "$1:")
|
|
raw = raw.indexOf("proxies:") === -1 ? "proxies:\n" + raw : "proxies:" + raw.split("proxies:")[1]
|
|
const proxies = YAML.eval(raw).proxies;
|
|
return proxies.map(p => JSON.stringify(p)).join("\n");
|
|
}
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function SSD() {
|
|
const name = "SSD Pre-processor";
|
|
const test = function (raw) {
|
|
return raw.indexOf("ssd://") === 0;
|
|
};
|
|
const parse = function (raw) {
|
|
// preprocessing for SSD subscription format
|
|
const output = [];
|
|
let ssdinfo = JSON.parse(Base64.safeDecode(raw.split("ssd://")[1]));
|
|
// options (traffic_used, traffic_total, expiry, url)
|
|
const traffic_used = ssdinfo.traffic_used; // GB
|
|
const traffic_total = ssdinfo.traffic_total; // GB, -1 means unlimited
|
|
const expiry = ssdinfo.expiry; // YYYY-MM-DD HH:mm:ss
|
|
// default setting
|
|
let name = ssdinfo.airport; // name of the airport
|
|
let port = ssdinfo.port;
|
|
let method = ssdinfo.encryption;
|
|
let password = ssdinfo.password;
|
|
// servers config
|
|
let servers = ssdinfo.servers;
|
|
for (let i = 0; i < servers.length; i++) {
|
|
let server = servers[i];
|
|
method = server.encryption ? server.encryption : method;
|
|
password = server.password ? server.password : password;
|
|
let userinfo = Base64.safeEncode(method + ":" + password);
|
|
let hostname = server.server;
|
|
port = server.port ? server.port : port;
|
|
let tag = server.remarks ? server.remarks : i;
|
|
let plugin = server.plugin_options
|
|
? "/?plugin=" +
|
|
encodeURIComponent(server.plugin + ";" + server.plugin_options)
|
|
: "";
|
|
output[i] =
|
|
"ss://" + userinfo + "@" + hostname + ":" + port + plugin + "#" + tag;
|
|
}
|
|
return output.join("\n");
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
return [
|
|
HTML(), Base64Encoded(), Clash(), SSD()
|
|
];
|
|
})();
|
|
const PROXY_PARSERS = (function () {
|
|
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
|
|
// reference: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
|
|
function URI_SS() {
|
|
const name = "URI SS Parser";
|
|
const test = (line) => {
|
|
return /^ss:\/\//.test(line);
|
|
};
|
|
const parse = (line) => {
|
|
const supported = {};
|
|
// parse url
|
|
let content = line.split("ss://")[1];
|
|
|
|
const proxy = {
|
|
name: decodeURIComponent(line.split("#")[1]),
|
|
type: "ss",
|
|
supported,
|
|
};
|
|
content = content.split("#")[0]; // strip proxy name
|
|
// handle IPV4 and IPV6
|
|
const serverAndPort = content.match(/@([^\/]*)(\/|$)/)[1];
|
|
const portIdx = serverAndPort.lastIndexOf(":");
|
|
proxy.server = serverAndPort.substring(0, portIdx);
|
|
proxy.port = serverAndPort.substring(portIdx + 1);
|
|
|
|
const userInfo = Base64.safeDecode(content.split("@")[0]).split(":");
|
|
proxy.cipher = userInfo[0];
|
|
proxy.password = userInfo[1];
|
|
|
|
// handle obfs
|
|
const idx = content.indexOf("?plugin=");
|
|
if (idx !== -1) {
|
|
const pluginInfo = (
|
|
"plugin=" +
|
|
decodeURIComponent(content.split("?plugin=")[1].split("&")[0])
|
|
).split(";");
|
|
const params = {};
|
|
for (const item of pluginInfo) {
|
|
const [key, val] = item.split("=");
|
|
if (key) params[key] = val || true; // some options like "tls" will not have value
|
|
}
|
|
switch (params.plugin) {
|
|
case "obfs-local":
|
|
case "simple-obfs":
|
|
proxy.plugin = "obfs";
|
|
proxy["plugin-opts"] = {
|
|
mode: params.obfs,
|
|
host: params["obfs-host"],
|
|
};
|
|
break;
|
|
case "v2ray-plugin":
|
|
proxy.supported = {
|
|
...supported,
|
|
Loon: false,
|
|
Surge: false,
|
|
};
|
|
proxy.obfs = "v2ray-plugin";
|
|
proxy["plugin-opts"] = {
|
|
mode: "websocket",
|
|
host: params["obfs-host"],
|
|
path: params.path || "",
|
|
tls: params.tls || false,
|
|
};
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported plugin option: ${params.plugin}`);
|
|
}
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
// Parse URI SSR format, such as ssr://xxx
|
|
function URI_SSR() {
|
|
const name = "URI SSR Parser";
|
|
const test = (line) => {
|
|
return /^ssr:\/\//.test(line);
|
|
};
|
|
const supported = {
|
|
Surge: false,
|
|
};
|
|
|
|
const parse = (line) => {
|
|
line = Base64.safeDecode(line.split("ssr://")[1]);
|
|
|
|
// handle IPV6 & IPV4 format
|
|
let splitIdx = line.indexOf(":origin");
|
|
if (splitIdx === -1) {
|
|
splitIdx = line.indexOf(":auth_");
|
|
}
|
|
const serverAndPort = line.substring(0, splitIdx);
|
|
const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
|
|
const port = serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1);
|
|
|
|
let params = line
|
|
.substring(splitIdx + 1)
|
|
.split("/?")[0]
|
|
.split(":");
|
|
let proxy = {
|
|
type: "ssr",
|
|
server,
|
|
port,
|
|
protocol: params[0],
|
|
cipher: params[1],
|
|
obfs: params[2],
|
|
password: Base64.safeDecode(params[3]),
|
|
supported,
|
|
};
|
|
// get other params
|
|
const other_params = {};
|
|
line = line.split("/?")[1].split("&");
|
|
if (line.length > 1) {
|
|
for (const item of line) {
|
|
const [key, val] = item.split("=");
|
|
other_params[key] = val.trim();
|
|
}
|
|
}
|
|
proxy = {
|
|
...proxy,
|
|
name: other_params.remarks ? Base64.safeDecode(other_params.remarks) : proxy.server,
|
|
"protocol-param":
|
|
Base64.safeDecode(other_params.protoparam || "").replace(/\s/g, ""),
|
|
"obfs-param":
|
|
Base64.safeDecode(other_params.obfsparam || "").replace(/\s/g, ""),
|
|
};
|
|
return proxy;
|
|
};
|
|
|
|
return {name, test, parse};
|
|
}
|
|
|
|
// V2rayN URI VMess format
|
|
// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
|
|
|
|
// Quantumult VMess format
|
|
function URI_VMess() {
|
|
const name = "URI VMess Parser";
|
|
const test = (line) => {
|
|
return /^vmess:\/\//.test(line);
|
|
};
|
|
const parse = (line) => {
|
|
const supported = {};
|
|
line = line.split("vmess://")[1];
|
|
const content = Base64.safeDecode(line);
|
|
if (/=\s*vmess/.test(content)) {
|
|
// Quantumult VMess URI format
|
|
const partitions = content.split(",").map((p) => p.trim());
|
|
// get keyword params
|
|
const params = {};
|
|
for (const part of partitions) {
|
|
if (part.indexOf("=") !== -1) {
|
|
const [key, val] = part.split("=");
|
|
params[key.trim()] = val.trim();
|
|
}
|
|
}
|
|
|
|
const proxy = {
|
|
name: partitions[0].split("=")[0].trim(),
|
|
type: "vmess",
|
|
server: partitions[1],
|
|
port: partitions[2],
|
|
cipher: partitions[3],
|
|
uuid: partitions[4].match(/^"(.*)"$/)[1],
|
|
tls: params.obfs === "over-tls" || params.obfs === "wss",
|
|
};
|
|
|
|
if (typeof params['udp-relay'] !== "undefined") proxy.udp = JSON.parse(params["udp-relay"]);
|
|
if (typeof params['fast-open'] !== "undefined") proxy.udp = JSON.parse(params["fast-open"]);
|
|
|
|
// handle ws headers
|
|
if (params.obfs === "ws" || params.obfs === "wss") {
|
|
proxy.network = "ws";
|
|
proxy["ws-path"] = (params["obfs-path"] || '"/"').match(/^"(.*)"$/)[1];
|
|
let obfs_host = params["obfs-header"];
|
|
if (obfs_host && obfs_host.indexOf("Host") !== -1) {
|
|
obfs_host = obfs_host.match(/Host:\s*([a-zA-Z0-9-.]*)/)[1];
|
|
}
|
|
proxy["ws-headers"] = {
|
|
Host: obfs_host || proxy.server, // if no host provided, use the same as server
|
|
};
|
|
}
|
|
|
|
// handle scert
|
|
if (proxy.tls && params['"tls-verification"'] === "false") {
|
|
proxy['skip-cert-verify'] = true;
|
|
}
|
|
|
|
// handle sni
|
|
if (proxy.tls && params["obfs-host"]) {
|
|
proxy.sni = params["obfs-host"];
|
|
}
|
|
|
|
return proxy;
|
|
} else {
|
|
// V2rayN URI format
|
|
const params = JSON.parse(content);
|
|
const proxy = {
|
|
name: params.ps,
|
|
type: "vmess",
|
|
server: params.add,
|
|
port: params.port,
|
|
cipher: "auto", // V2rayN has no default cipher! use aes-128-gcm as default.
|
|
uuid: params.id,
|
|
alterId: params.aid || 0,
|
|
tls: params.tls === "tls" || params.tls === true,
|
|
supported,
|
|
};
|
|
// handle obfs
|
|
if (params.net === "ws") {
|
|
proxy.network = "ws";
|
|
proxy["ws-path"] = params.path;
|
|
proxy["ws-headers"] = {
|
|
Host: params.host || params.add,
|
|
};
|
|
if (proxy.tls && params.host) {
|
|
proxy.sni = params.host;
|
|
}
|
|
}
|
|
// handle scert
|
|
if (params.verify_cert === false) {
|
|
proxy['skip-cert-verify'] = true;
|
|
}
|
|
return proxy;
|
|
}
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
// Trojan URI format
|
|
function URI_Trojan() {
|
|
const name = "URI Trojan Parser";
|
|
const test = (line) => {
|
|
return /^trojan:\/\//.test(line);
|
|
};
|
|
|
|
const parse = (line) => {
|
|
const supported = {};
|
|
line = line.split("trojan://")[1];
|
|
const [server, port] = line.split("@")[1].split("?")[0].split(":");
|
|
const name = decodeURIComponent(line.split("#")[1].trim());
|
|
|
|
return {
|
|
name: name || `[Trojan] ${server}`, // trojan uri may have no server tag!
|
|
type: "trojan",
|
|
server,
|
|
port,
|
|
password: line.split("@")[0],
|
|
supported,
|
|
};
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Clash_All() {
|
|
const name = "Clash Parser";
|
|
const test = (line) => {
|
|
try {
|
|
JSON.parse(line);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
const parse = (line) => JSON.parse(line);
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function QX_SS() {
|
|
const name = "QX SS Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^shadowsocks\s*=/.test(line.split(",")[0].trim()) &&
|
|
line.indexOf("ssr-protocol") === -1
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
const supported = {};
|
|
const params = getQXParams(line);
|
|
const proxy = {
|
|
name: params.tag,
|
|
type: "ss",
|
|
server: params.server,
|
|
port: params.port,
|
|
cipher: params.method,
|
|
password: params.password,
|
|
udp: JSON.parse(params["udp-relay"] || "false"),
|
|
tfo: JSON.parse(params["fast-open"] || "false"),
|
|
supported,
|
|
};
|
|
// handle obfs options
|
|
if (params.obfs) {
|
|
proxy["plugin-opts"] = {
|
|
host: params["obfs-host"] || proxy.server,
|
|
};
|
|
switch (params.obfs) {
|
|
case "http":
|
|
case "tls":
|
|
proxy.plugin = "obfs";
|
|
proxy["plugin-opts"].mode = params.obfs;
|
|
break;
|
|
case "ws":
|
|
case "wss":
|
|
proxy["plugin-opts"] = {
|
|
...proxy["plugin-opts"],
|
|
mode: "websocket",
|
|
path: params["obfs-uri"] || "/",
|
|
tls: params.obfs === "wss",
|
|
};
|
|
if (proxy["plugin-opts"].tls && typeof params['tls-verification'] !== "undefined") {
|
|
proxy["plugin-opts"]['skip-cert-verify'] = params['tls-verification'];
|
|
}
|
|
proxy.plugin = "v2ray-plugin";
|
|
// Surge and Loon lack support for v2ray-plugin obfs
|
|
proxy.supported.Surge = false;
|
|
proxy.supported.Loon = false;
|
|
break;
|
|
}
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function QX_SSR() {
|
|
const name = "QX SSR Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^shadowsocks\s*=/.test(line.split(",")[0].trim()) &&
|
|
line.indexOf("ssr-protocol") !== -1
|
|
);
|
|
};
|
|
|
|
const parse = (line) => {
|
|
const supported = {
|
|
Surge: false,
|
|
};
|
|
const params = getQXParams(line);
|
|
const proxy = {
|
|
name: params.tag,
|
|
type: "ssr",
|
|
server: params.server,
|
|
port: params.port,
|
|
cipher: params.method,
|
|
password: params.password,
|
|
protocol: params["ssr-protocol"],
|
|
obfs: "plain", // default obfs
|
|
"protocol-param": params["ssr-protocol-param"],
|
|
udp: JSON.parse(params["udp-relay"] || "false"),
|
|
tfo: JSON.parse(params["fast-open"] || "false"),
|
|
supported,
|
|
};
|
|
// handle obfs options
|
|
if (params.obfs) {
|
|
proxy.obfs = params.obfs;
|
|
proxy["obfs-param"] = params["obfs-host"];
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function QX_VMess() {
|
|
const name = "QX VMess Parser";
|
|
const test = (line) => {
|
|
return /^vmess\s*=/.test(line.split(",")[0].trim());
|
|
};
|
|
const parse = (line) => {
|
|
const params = getQXParams(line);
|
|
const proxy = {
|
|
type: "vmess",
|
|
name: params.tag,
|
|
server: params.server,
|
|
port: params.port,
|
|
cipher: params.method || "none",
|
|
uuid: params.password,
|
|
alterId: 0,
|
|
tls: params.obfs === "over-tls" || params.obfs === "wss",
|
|
udp: JSON.parse(params["udp-relay"] || "false"),
|
|
tfo: JSON.parse(params["fast-open"] || "false"),
|
|
};
|
|
if (proxy.tls) {
|
|
proxy.sni = params["obfs-host"] || params.server;
|
|
proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true");
|
|
}
|
|
// handle ws headers
|
|
if (params.obfs === "ws" || params.obfs === "wss") {
|
|
proxy.network = "ws";
|
|
proxy["ws-path"] = params["obfs-uri"];
|
|
proxy["ws-headers"] = {
|
|
Host: params["obfs-host"] || params.server, // if no host provided, use the same as server
|
|
};
|
|
}
|
|
return proxy;
|
|
};
|
|
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function QX_Trojan() {
|
|
const name = "QX Trojan Parser";
|
|
const test = (line) => {
|
|
return /^trojan\s*=/.test(line.split(",")[0].trim());
|
|
};
|
|
const parse = (line) => {
|
|
const params = getQXParams(line);
|
|
const proxy = {
|
|
type: "trojan",
|
|
name: params.tag,
|
|
server: params.server,
|
|
port: params.port,
|
|
password: params.password,
|
|
sni: params["tls-host"] || params.server,
|
|
udp: JSON.parse(params["udp-relay"] || "false"),
|
|
tfo: JSON.parse(params["fast-open"] || "false"),
|
|
};
|
|
proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true");
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function QX_Http() {
|
|
const name = "QX HTTP Parser";
|
|
const test = (line) => {
|
|
return /^http\s*=/.test(line.split(",")[0].trim());
|
|
};
|
|
const parse = (line) => {
|
|
const params = getQXParams(line);
|
|
const proxy = {
|
|
type: "http",
|
|
name: params.tag,
|
|
server: params.server,
|
|
port: params.port,
|
|
tls: JSON.parse(params["over-tls"] || "false"),
|
|
udp: JSON.parse(params["udp-relay"] || "false"),
|
|
tfo: JSON.parse(params["fast-open"] || "false"),
|
|
};
|
|
if (params.username && params.username !== 'none') proxy.username = params.username;
|
|
if (params.password && params.password !== 'none') proxy.password = params.password;
|
|
if (proxy.tls) {
|
|
proxy.sni = params["tls-host"] || proxy.server;
|
|
proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true");
|
|
}
|
|
return proxy;
|
|
};
|
|
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function getQXParams(line) {
|
|
const groups = line.split(",");
|
|
const params = {};
|
|
const protocols = ["shadowsocks", "vmess", "http", "trojan"];
|
|
groups.forEach((g) => {
|
|
let [key, value] = g.split("=");
|
|
key = key.trim();
|
|
value = value.trim();
|
|
if (protocols.indexOf(key) !== -1) {
|
|
params.type = key;
|
|
const conf = value.split(":");
|
|
params.server = conf[0];
|
|
params.port = conf[1];
|
|
} else {
|
|
params[key.trim()] = value.trim();
|
|
}
|
|
});
|
|
return params;
|
|
}
|
|
|
|
function Loon_SS() {
|
|
const name = "Loon SS Parser";
|
|
const test = (line) => {
|
|
return (
|
|
line.split(",")[0].split("=")[1].trim().toLowerCase() === "shadowsocks"
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
const params = line.split("=")[1].split(",");
|
|
const proxy = {
|
|
name: line.split("=")[0].trim(),
|
|
type: "ss",
|
|
server: params[1],
|
|
port: params[2],
|
|
cipher: params[3],
|
|
password: params[4].replace(/"/g, ""),
|
|
};
|
|
// handle obfs
|
|
if (params.length > 5) {
|
|
proxy.plugin = "obfs";
|
|
proxy["plugin-opts"] = {
|
|
mode: params[5],
|
|
host: params[6],
|
|
};
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Loon_SSR() {
|
|
const name = "Loon SSR Parser";
|
|
const test = (line) => {
|
|
return (
|
|
line.split(",")[0].split("=")[1].trim().toLowerCase() === "shadowsocksr"
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
const params = line.split("=")[1].split(",");
|
|
const supported = {
|
|
Surge: false,
|
|
};
|
|
return {
|
|
name: line.split("=")[0].trim(),
|
|
type: "ssr",
|
|
server: params[1],
|
|
port: params[2],
|
|
cipher: params[3],
|
|
password: params[4].replace(/"/g, ""),
|
|
protocol: params[5],
|
|
"protocol-param": params[6].match(/{(.*)}/)[1],
|
|
supported,
|
|
obfs: params[7],
|
|
"obfs-param": params[8].match(/{(.*)}/)[1],
|
|
};
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Loon_VMess() {
|
|
const name = "Loon VMess Parser";
|
|
const test = (line) => {
|
|
// distinguish between surge vmess
|
|
return (
|
|
/^.*=\s*vmess/i.test(line.split(",")[0]) &&
|
|
line.indexOf("username") === -1
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
let params = line.split("=")[1].split(",");
|
|
const proxy = {
|
|
name: line.split("=")[0].trim(),
|
|
type: "vmess",
|
|
server: params[1],
|
|
port: params[2],
|
|
cipher: params[3] || "none",
|
|
uuid: params[4].replace(/"/g, ""),
|
|
alterId: 0,
|
|
};
|
|
// get transport options
|
|
params = params.splice(5);
|
|
for (const item of params) {
|
|
const [key, val] = item.split(":");
|
|
params[key] = val;
|
|
}
|
|
proxy.tls = JSON.parse(params["over-tls"] || "false");
|
|
if (proxy.tls) {
|
|
proxy.sni = params["tls-name"] || proxy.server;
|
|
proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false");
|
|
}
|
|
switch (params.transport) {
|
|
case "tcp":
|
|
break;
|
|
case "ws":
|
|
proxy.network = params.transport;
|
|
proxy["ws-path"] = params.path;
|
|
proxy["ws-headers"] = {
|
|
Host: params.host,
|
|
};
|
|
}
|
|
if (proxy.tls) {
|
|
proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false");
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Loon_Trojan() {
|
|
const name = "Loon Trojan Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^.*=\s*trojan/i.test(line.split(",")[0]) &&
|
|
line.indexOf("password") === -1
|
|
);
|
|
};
|
|
|
|
const parse = (line) => {
|
|
const params = line.split("=")[1].split(",");
|
|
const proxy = {
|
|
name: line.split("=")[0].trim(),
|
|
type: "trojan",
|
|
server: params[1],
|
|
port: params[2],
|
|
password: params[3].replace(/"/g, ""),
|
|
sni: params[1], // default sni is the server itself
|
|
"skip-cert-verify": JSON.parse(params["skip-cert-verify"] || "false"),
|
|
};
|
|
// trojan sni
|
|
if (params.length > 4) {
|
|
const [key, val] = params[4].split(":");
|
|
if (key === "tls-name") proxy.sni = val;
|
|
else throw new Error(`Unknown option ${key} for line: \n${line}`);
|
|
}
|
|
return proxy;
|
|
};
|
|
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Loon_Http() {
|
|
const name = "Loon HTTP Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^.*=\s*http/i.test(line.split(",")[0]) &&
|
|
line.split(",").length === 5 &&
|
|
line.indexOf("username") === -1 &&
|
|
line.indexOf("password") === -1
|
|
);
|
|
};
|
|
|
|
const parse = (line) => {
|
|
const params = line.split("=")[1].split(",");
|
|
const proxy = {
|
|
name: line.split("=")[0].trim(),
|
|
type: "http",
|
|
server: params[1],
|
|
port: params[2],
|
|
tls: params[2] === "443", // port 443 is considered as https type
|
|
};
|
|
if (params[3]) proxy.username = params[3];
|
|
if (params[4]) proxy.password = params[4];
|
|
|
|
if (proxy.tls) {
|
|
proxy.sni = params["tls-name"] || proxy.server;
|
|
proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false");
|
|
}
|
|
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Surge_SS() {
|
|
const name = "Surge SS Parser";
|
|
const test = (line) => {
|
|
return /^.*=\s*ss/.test(line.split(",")[0]);
|
|
};
|
|
const parse = (line) => {
|
|
const params = getSurgeParams(line);
|
|
const proxy = {
|
|
name: params.name,
|
|
type: "ss",
|
|
server: params.server,
|
|
port: params.port,
|
|
cipher: params["encrypt-method"],
|
|
password: params.password,
|
|
tfo: JSON.parse(params.tfo || "false"),
|
|
udp: JSON.parse(params["udp-relay"] || "false"),
|
|
};
|
|
// handle obfs
|
|
if (params.obfs) {
|
|
proxy.plugin = "obfs";
|
|
proxy["plugin-opts"] = {
|
|
mode: params.obfs,
|
|
host: params["obfs-host"],
|
|
};
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Surge_VMess() {
|
|
const name = "Surge VMess Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
const params = getSurgeParams(line);
|
|
const proxy = {
|
|
name: params.name,
|
|
type: "vmess",
|
|
server: params.server,
|
|
port: params.port,
|
|
uuid: params.username,
|
|
alterId: 0, // surge does not have this field
|
|
cipher: "none", // surge does not have this field
|
|
tls: JSON.parse(params.tls || "false"),
|
|
tfo: JSON.parse(params.tfo || "false"),
|
|
};
|
|
if (proxy.tls) {
|
|
if (typeof params["skip-cert-verify"] !== "undefined") {
|
|
proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1";
|
|
}
|
|
proxy.sni = params["sni"] || params.server;
|
|
}
|
|
// use websocket
|
|
if (JSON.parse(params.ws || "false")) {
|
|
proxy.network = "ws";
|
|
proxy["ws-path"] = params["ws-path"];
|
|
const res = params["ws-headers"].match(/(,|^|\s)*HOST:\s*(.*?)(,|$)/);
|
|
const host = res ? res[2] : proxy.server;
|
|
proxy["ws-headers"] = {
|
|
Host: host || params.server,
|
|
};
|
|
}
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Surge_Trojan() {
|
|
const name = "Surge Trojan Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
const params = getSurgeParams(line);
|
|
const proxy = {
|
|
name: params.name,
|
|
type: "trojan",
|
|
server: params.server,
|
|
port: params.port,
|
|
password: params.password,
|
|
sni: params.sni || params.server,
|
|
tfo: JSON.parse(params.tfo || "false"),
|
|
};
|
|
if (typeof params["skip-cert-verify"] !== "undefined") {
|
|
proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1";
|
|
}
|
|
return proxy;
|
|
};
|
|
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function Surge_Http() {
|
|
const name = "Surge HTTP Parser";
|
|
const test = (line) => {
|
|
return (
|
|
/^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().test(line)
|
|
);
|
|
};
|
|
const parse = (line) => {
|
|
const params = getSurgeParams(line);
|
|
const proxy = {
|
|
name: params.name,
|
|
type: "http",
|
|
server: params.server,
|
|
port: params.port,
|
|
tls: JSON.parse(params.tls || "false"),
|
|
tfo: JSON.parse(params.tfo || "false"),
|
|
};
|
|
if (proxy.tls) {
|
|
if (typeof params["skip-cert-verify"] !== "undefined") {
|
|
proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1";
|
|
}
|
|
proxy.sni = params.sni || params.server;
|
|
}
|
|
if (params.username && params.username !== "none") proxy.username = params.username;
|
|
if (params.password && params.password !== "none") proxy.password = params.password;
|
|
return proxy;
|
|
};
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function getSurgeParams(line) {
|
|
const params = {};
|
|
params.name = line.split("=")[0].trim();
|
|
const segments = line.split(",");
|
|
params.server = segments[1].trim();
|
|
params.port = segments[2].trim();
|
|
for (let i = 3; i < segments.length; i++) {
|
|
const item = segments[i];
|
|
if (item.indexOf("=") !== -1) {
|
|
const [key, value] = item.split("=");
|
|
params[key.trim()] = value.trim();
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
return [
|
|
URI_SS(), URI_SSR(), URI_VMess(), URI_Trojan(),
|
|
Clash_All(),
|
|
Surge_SS(), Surge_VMess(), Surge_Trojan(), Surge_Http(),
|
|
Loon_SS(), Loon_SSR(), Loon_VMess(), Loon_Trojan(), Loon_Http(),
|
|
QX_SS(), QX_SSR(), QX_VMess(), QX_Trojan(), QX_Http()
|
|
];
|
|
})();
|
|
const PROXY_PROCESSORS = (function () {
|
|
// force to set some properties (e.g., skip-cert-verify, udp, tfo, etc.)
|
|
function SetPropertyOperator({key, value}) {
|
|
return {
|
|
name: "Set Property Operator",
|
|
func: (proxies) => {
|
|
return proxies.map((p) => {
|
|
p[key] = value;
|
|
return p;
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// add or remove flag for proxies
|
|
function FlagOperator(add = true) {
|
|
return {
|
|
name: "Flag Operator",
|
|
func: (proxies) => {
|
|
return proxies.map((proxy) => {
|
|
if (!add) {
|
|
// no flag
|
|
proxy.name = removeFlag(proxy.name);
|
|
} else {
|
|
// get flag
|
|
const newFlag = getFlag(proxy.name);
|
|
// remove old flag
|
|
proxy.name = removeFlag(proxy.name);
|
|
proxy.name = newFlag + " " + proxy.name;
|
|
proxy.name = proxy.name.replace(/🇹🇼/g, "🇨🇳");
|
|
}
|
|
return proxy;
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// sort proxies according to their names
|
|
function SortOperator(order = "asc") {
|
|
return {
|
|
name: "Sort Operator",
|
|
func: (proxies) => {
|
|
switch (order) {
|
|
case "asc":
|
|
case "desc":
|
|
return proxies.sort((a, b) => {
|
|
let res = a.name > b.name ? 1 : -1;
|
|
res *= order === "desc" ? -1 : 1;
|
|
return res;
|
|
});
|
|
case "random":
|
|
return shuffle(proxies);
|
|
default:
|
|
throw new Error("Unknown sort option: " + order);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
// sort by regex
|
|
function RegexSortOperator(expressions) {
|
|
return {
|
|
name: "Regex Sort Operator",
|
|
func: (proxies) => {
|
|
expressions = expressions.map(expr => buildRegex(expr));
|
|
return proxies.sort((a, b) => {
|
|
const oA = getRegexOrder(expressions, a.name);
|
|
const oB = getRegexOrder(expressions, b.name);
|
|
if (oA && !oB) return -1;
|
|
if (oB && !oA) return 1;
|
|
if (oA && oB) return oA < oB ? -1 : 1;
|
|
if ((!oA && !oB) || (oA && oB && oA === oB))
|
|
return a.name < b.name ? -1 : 1; // fallback to normal sort
|
|
})
|
|
}
|
|
|
|
};
|
|
}
|
|
|
|
function getRegexOrder(expressions, str) {
|
|
let order = null;
|
|
for (let i = 0; i < expressions.length; i++) {
|
|
if (expressions[i].test(str)) {
|
|
order = i + 1; // plus 1 is important! 0 will be treated as false!!!
|
|
break;
|
|
}
|
|
}
|
|
return order;
|
|
}
|
|
|
|
// rename by regex
|
|
// keywords: [{expr: "string format regex", now: "now"}]
|
|
function RegexRenameOperator(regex) {
|
|
return {
|
|
name: "Regex Rename Operator",
|
|
func: (proxies) => {
|
|
return proxies.map((proxy) => {
|
|
for (const {expr, now} of regex) {
|
|
proxy.name = proxy.name.replace(buildRegex(expr, "g"), now).trim();
|
|
}
|
|
return proxy;
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// delete regex operator
|
|
// regex: ['a', 'b', 'c']
|
|
function RegexDeleteOperator(regex) {
|
|
const regex_ = regex.map((r) => {
|
|
return {
|
|
expr: r,
|
|
now: "",
|
|
};
|
|
});
|
|
return {
|
|
name: "Regex Delete Operator",
|
|
func: RegexRenameOperator(regex_).func,
|
|
};
|
|
}
|
|
|
|
// use base64 encoded script to rename
|
|
/** Example script
|
|
function operator(proxies) {
|
|
// do something
|
|
return proxies;
|
|
}
|
|
|
|
WARNING:
|
|
1. This function name should be `operator`!
|
|
2. Always declare variables before using them!
|
|
*/
|
|
function ScriptOperator(script) {
|
|
return {
|
|
name: "Script Operator",
|
|
func: (proxies) => {
|
|
let output = proxies;
|
|
(function () {
|
|
// interface to get internal operators
|
|
const $get = (name, args) => {
|
|
const item = PROXY_PROCESSORS[name];
|
|
return item(args);
|
|
};
|
|
const $process = ApplyProcessor;
|
|
eval(script);
|
|
output = operator(proxies);
|
|
})();
|
|
return output;
|
|
},
|
|
};
|
|
}
|
|
|
|
/**************************** Filters ***************************************/
|
|
// filter useless proxies
|
|
function UselessFilter() {
|
|
const KEYWORDS = [
|
|
"网址",
|
|
"流量",
|
|
"时间",
|
|
"应急",
|
|
"过期",
|
|
"Bandwidth",
|
|
"expire",
|
|
];
|
|
return {
|
|
name: "Useless Filter",
|
|
func: RegexFilter({
|
|
regex: KEYWORDS,
|
|
keep: false,
|
|
}).func,
|
|
};
|
|
}
|
|
|
|
// filter by regions
|
|
function RegionFilter(regions) {
|
|
const REGION_MAP = {
|
|
HK: "🇭🇰",
|
|
TW: "🇹🇼",
|
|
US: "🇺🇸",
|
|
SG: "🇸🇬",
|
|
JP: "🇯🇵",
|
|
UK: "🇬🇧",
|
|
};
|
|
return {
|
|
name: "Region Filter",
|
|
func: (proxies) => {
|
|
// this would be high memory usage
|
|
return proxies.map((proxy) => {
|
|
const flag = getFlag(proxy.name);
|
|
return regions.some((r) => REGION_MAP[r] === flag);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// filter by regex
|
|
function RegexFilter({regex = [], keep = true}) {
|
|
return {
|
|
name: "Regex Filter",
|
|
func: (proxies) => {
|
|
return proxies.map((proxy) => {
|
|
const selected = regex.some((r) => {
|
|
return buildRegex(r).test(proxy.name);
|
|
});
|
|
return keep ? selected : !selected;
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// filter by proxy types
|
|
function TypeFilter(types) {
|
|
return {
|
|
name: "Type Filter",
|
|
func: (proxies) => {
|
|
return proxies.map((proxy) => types.some((t) => proxy.type === t));
|
|
},
|
|
};
|
|
}
|
|
|
|
// use base64 encoded script to filter proxies
|
|
/**
|
|
Script Example
|
|
function func(proxies) {
|
|
const selected = FULL(proxies.length, true);
|
|
// do something
|
|
return selected;
|
|
}
|
|
WARNING:
|
|
1. This function name should be `func`!
|
|
2. Always declare variables before using them!
|
|
*/
|
|
function ScriptFilter(script) {
|
|
return {
|
|
name: "Script Filter",
|
|
func: (proxies) => {
|
|
let output = FULL(proxies.length, true);
|
|
!(function () {
|
|
eval(script);
|
|
output = filter(proxies);
|
|
})();
|
|
return output;
|
|
},
|
|
};
|
|
}
|
|
|
|
/******************************** Utility Functions *********************************************/
|
|
// get proxy flag according to its name
|
|
function getFlag(name) {
|
|
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
|
|
const flags = {
|
|
"🇦🇨": ["AC"],
|
|
"🇦🇹": ["奥地利", "维也纳"],
|
|
"🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"],
|
|
"🇧🇪": ["BE", "比利时"],
|
|
"🇧🇬": ["保加利亚", "Bulgaria"],
|
|
"🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"],
|
|
"🇨🇦": [
|
|
"CA",
|
|
"Canada",
|
|
"Waterloo",
|
|
"加拿大",
|
|
"蒙特利尔",
|
|
"温哥华",
|
|
"楓葉",
|
|
"枫叶",
|
|
"滑铁卢",
|
|
"多伦多",
|
|
],
|
|
"🇨🇭": ["瑞士", "苏黎世", "Switzerland"],
|
|
"🇩🇪": ["DE", "German", "GERMAN", "德国", "德國", "法兰克福"],
|
|
"🇩🇰": ["丹麦"],
|
|
"🇪🇸": ["ES", "西班牙", "Spain"],
|
|
"🇪🇺": ["EU", "欧盟", "欧罗巴"],
|
|
"🇫🇮": ["Finland", "芬兰", "赫尔辛基"],
|
|
"🇫🇷": ["FR", "France", "法国", "法國", "巴黎"],
|
|
"🇬🇧": ["UK", "GB", "England", "United Kingdom", "英国", "伦敦", "英"],
|
|
"🇲🇴": ["MO", "Macao", "澳门", "CTM"],
|
|
"🇭🇺": ["匈牙利", "Hungary"],
|
|
"🇭🇰": [
|
|
"HK",
|
|
"Hongkong",
|
|
"Hong Kong",
|
|
"香港",
|
|
"深港",
|
|
"沪港",
|
|
"呼港",
|
|
"HKT",
|
|
"HKBN",
|
|
"HGC",
|
|
"WTT",
|
|
"CMI",
|
|
"穗港",
|
|
"京港",
|
|
"港",
|
|
],
|
|
"🇮🇩": ["Indonesia", "印尼", "印度尼西亚", "雅加达"],
|
|
"🇮🇪": ["Ireland", "爱尔兰", "都柏林"],
|
|
"🇮🇳": ["India", "印度", "孟买", "Mumbai"],
|
|
"🇰🇵": ["KP", "朝鲜"],
|
|
"🇰🇷": ["KR", "Korea", "KOR", "韩国", "首尔", "韩", "韓"],
|
|
"🇱🇻": ["Latvia", "Latvija", "拉脱维亚"],
|
|
"🇲🇽️": ["MEX", "MX", "墨西哥"],
|
|
"🇲🇾": ["MY", "Malaysia", "马来西亚", "吉隆坡"],
|
|
"🇳🇱": ["NL", "Netherlands", "荷兰", "荷蘭", "尼德蘭", "阿姆斯特丹"],
|
|
"🇵🇭": ["PH", "Philippines", "菲律宾"],
|
|
"🇷🇴": ["RO", "罗马尼亚"],
|
|
"🇷🇺": [
|
|
"RU",
|
|
"Russia",
|
|
"俄罗斯",
|
|
"俄羅斯",
|
|
"伯力",
|
|
"莫斯科",
|
|
"圣彼得堡",
|
|
"西伯利亚",
|
|
"新西伯利亚",
|
|
"京俄",
|
|
"杭俄",
|
|
],
|
|
"🇸🇦": ["沙特", "迪拜"],
|
|
"🇸🇪": ["SE", "Sweden"],
|
|
"🇸🇬": [
|
|
"SG",
|
|
"Singapore",
|
|
"新加坡",
|
|
"狮城",
|
|
"沪新",
|
|
"京新",
|
|
"泉新",
|
|
"穗新",
|
|
"深新",
|
|
"杭新",
|
|
"广新",
|
|
],
|
|
"🇹🇭": ["TH", "Thailand", "泰国", "泰國", "曼谷"],
|
|
"🇹🇷": ["TR", "Turkey", "土耳其", "伊斯坦布尔"],
|
|
"🇹🇼": [
|
|
"TW",
|
|
"Taiwan",
|
|
"台湾",
|
|
"台北",
|
|
"台中",
|
|
"新北",
|
|
"彰化",
|
|
"CHT",
|
|
"台",
|
|
"HINET",
|
|
],
|
|
"🇺🇸": [
|
|
"US",
|
|
"USA",
|
|
"America",
|
|
"United States",
|
|
"美国",
|
|
"美",
|
|
"京美",
|
|
"波特兰",
|
|
"达拉斯",
|
|
"俄勒冈",
|
|
"凤凰城",
|
|
"费利蒙",
|
|
"硅谷",
|
|
"矽谷",
|
|
"拉斯维加斯",
|
|
"洛杉矶",
|
|
"圣何塞",
|
|
"圣克拉拉",
|
|
"西雅图",
|
|
"芝加哥",
|
|
"沪美",
|
|
"哥伦布",
|
|
"纽约",
|
|
],
|
|
"🇻🇳": ["VN", "越南", "胡志明市"],
|
|
"🇮🇹": ["Italy", "IT", "Nachash", "意大利", "米兰", "義大利"],
|
|
"🇿🇦": ["South Africa", "南非"],
|
|
"🇦🇪": ["United Arab Emirates", "阿联酋"],
|
|
"🇯🇵": [
|
|
"JP",
|
|
"Japan",
|
|
"日",
|
|
"日本",
|
|
"东京",
|
|
"大阪",
|
|
"埼玉",
|
|
"沪日",
|
|
"穗日",
|
|
"川日",
|
|
"中日",
|
|
"泉日",
|
|
"杭日",
|
|
"深日",
|
|
"辽日",
|
|
"广日",
|
|
],
|
|
"🇦🇷": ["AR", "阿根廷"],
|
|
"🇳🇴": ["Norway", "挪威", "NO"],
|
|
"🇨🇳": [
|
|
"CN",
|
|
"China",
|
|
"回国",
|
|
"中国",
|
|
"江苏",
|
|
"北京",
|
|
"上海",
|
|
"广州",
|
|
"深圳",
|
|
"杭州",
|
|
"徐州",
|
|
"青岛",
|
|
"宁波",
|
|
"镇江",
|
|
"back",
|
|
],
|
|
"🏳️🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"],
|
|
};
|
|
for (let k of Object.keys(flags)) {
|
|
if (flags[k].some((item) => name.indexOf(item) !== -1)) {
|
|
return k;
|
|
}
|
|
}
|
|
// no flag found
|
|
const oldFlag = (name.match(
|
|
/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/
|
|
) || [])[0];
|
|
return oldFlag || "🏴☠️";
|
|
}
|
|
|
|
// remove flag
|
|
function removeFlag(str) {
|
|
return str
|
|
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, "")
|
|
.trim();
|
|
}
|
|
|
|
// shuffle array
|
|
function shuffle(array) {
|
|
let currentIndex = array.length,
|
|
temporaryValue,
|
|
randomIndex;
|
|
|
|
// While there remain elements to shuffle...
|
|
while (0 !== currentIndex) {
|
|
// Pick a remaining element...
|
|
randomIndex = Math.floor(Math.random() * currentIndex);
|
|
currentIndex -= 1;
|
|
|
|
// And swap it with the current element.
|
|
temporaryValue = array[currentIndex];
|
|
array[currentIndex] = array[randomIndex];
|
|
array[randomIndex] = temporaryValue;
|
|
}
|
|
|
|
return array;
|
|
}
|
|
|
|
return {
|
|
"Useless Filter": UselessFilter,
|
|
"Region Filter": RegionFilter,
|
|
"Regex Filter": RegexFilter,
|
|
"Type Filter": TypeFilter,
|
|
"Script Filter": ScriptFilter,
|
|
|
|
"Set Property Operator": SetPropertyOperator,
|
|
"Flag Operator": FlagOperator,
|
|
"Sort Operator": SortOperator,
|
|
"Regex Sort Operator": RegexSortOperator,
|
|
"Regex Rename Operator": RegexRenameOperator,
|
|
"Regex Delete Operator": RegexDeleteOperator,
|
|
"Script Operator": ScriptOperator,
|
|
};
|
|
})();
|
|
const PROXY_PRODUCERS = (function () {
|
|
function QX_Producer() {
|
|
const targetPlatform = "QX";
|
|
const produce = (proxy) => {
|
|
let obfs_opts;
|
|
let tls_opts;
|
|
switch (proxy.type) {
|
|
case "ss":
|
|
obfs_opts = "";
|
|
if (proxy.plugin === "obfs") {
|
|
const {host, mode} = proxy['plugin-opts'];
|
|
obfs_opts = `,obfs=${mode}${
|
|
host ? ",obfs-host=" + host : ""
|
|
}`;
|
|
}
|
|
if (proxy.plugin === "v2ray-plugin") {
|
|
const {tls, host, path} = proxy["plugin-opts"];
|
|
obfs_opts = `,obfs=${tls ? "wss" : "ws"}${
|
|
host ? ",obfs-host=" + host : ""
|
|
}${
|
|
path ? ",obfs-uri=" + path : ""
|
|
}`;
|
|
}
|
|
return `shadowsocks=${proxy.server}:${proxy.port},method=${
|
|
proxy.cipher
|
|
},password=${proxy.password}${obfs_opts}${
|
|
proxy.tfo ? ",fast-open=true" : ",fast-open=false"
|
|
}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${
|
|
proxy.name
|
|
}`;
|
|
case "ssr":
|
|
return `shadowsocks=${proxy.server}:${proxy.port},method=${
|
|
proxy.cipher
|
|
},password=${proxy.password},ssr-protocol=${proxy.protocol}${
|
|
proxy["protocol-param"]
|
|
? ",ssr-protocol-param=" + proxy["protocol-param"]
|
|
: ""
|
|
}${proxy.obfs ? ",obfs=" + proxy.obfs : ""}${
|
|
proxy["obfs-param"] ? ",obfs-host=" + proxy["obfs-param"] : ""
|
|
}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${
|
|
proxy.udp ? ",udp-relay=true" : ",udp-relay=false"
|
|
},tag=${proxy.name}`;
|
|
case "vmess":
|
|
obfs_opts = "";
|
|
if (proxy.network === "ws") {
|
|
// websocket
|
|
if (proxy.tls) {
|
|
// ws-tls
|
|
obfs_opts = `,obfs=wss${
|
|
proxy.sni ? ",obfs-host=" + proxy.sni : ""
|
|
}${
|
|
proxy["ws-path"] ? ",obfs-uri=" + proxy["ws-path"] : ""
|
|
},tls-verification=${proxy['skip-cert-verify'] ? "false" : "true"}`;
|
|
} else {
|
|
// ws
|
|
obfs_opts = `,obfs=ws${
|
|
proxy["ws-headers"].Host ? ",obfs-host=" + proxy["ws-headers"].Host : ""
|
|
}${
|
|
proxy["ws-path"] ? ",obfs-uri=" + proxy["ws-path"] : ""
|
|
}`;
|
|
}
|
|
} else {
|
|
// tcp
|
|
if (proxy.tls) {
|
|
obfs_opts = `,obfs=over-tls${
|
|
proxy.sni ? ",obfs-host=" + proxy.sni : ""
|
|
},tls-verification=${proxy['skip-cert-verify'] ? "false" : "true"}`;
|
|
}
|
|
}
|
|
return `vmess=${proxy.server}:${proxy.port},method=${
|
|
proxy.cipher === "auto" ? "none" : proxy.cipher
|
|
},password=${proxy.uuid}${obfs_opts}${
|
|
proxy.tfo ? ",fast-open=true" : ",fast-open=false"
|
|
}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${
|
|
proxy.name
|
|
}`;
|
|
case "trojan":
|
|
return `trojan=${proxy.server}:${proxy.port},password=${
|
|
proxy.password
|
|
}${proxy.sni ? ",tls-host=" + proxy.sni : ""},over-tls=true,tls-verification=${
|
|
proxy['skip-cert-verify'] ? "false" : "true"
|
|
}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${
|
|
proxy.udp ? ",udp-relay=true" : ",udp-relay=false"
|
|
},tag=${proxy.name}`;
|
|
case "http":
|
|
tls_opts = "";
|
|
if (proxy.tls) {
|
|
tls_opts = `,over-tls=true,tls-verification=${
|
|
proxy['skip-cert-verify'] ? "false" : "true"
|
|
}${
|
|
proxy.sni ? ",tls-host=" + proxy.sni : ""
|
|
}`;
|
|
}
|
|
return `http=${proxy.server}:${proxy.port},username=${
|
|
proxy.username
|
|
},password=${proxy.password}${tls_opts}${
|
|
proxy.tfo ? ",fast-open=true" : ",fast-open=false"
|
|
},tag=${proxy.name}`;
|
|
}
|
|
throw new Error(
|
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`
|
|
);
|
|
};
|
|
return {produce};
|
|
}
|
|
|
|
function Loon_Producer() {
|
|
const targetPlatform = "Loon";
|
|
const produce = (proxy) => {
|
|
let obfs_opts, tls_opts;
|
|
const udp_opts = proxy.udp ? ",udp=true" : "";
|
|
switch (proxy.type) {
|
|
case "ss":
|
|
obfs_opts = ",,";
|
|
if (proxy.plugin) {
|
|
if (proxy.plugin === "obfs") {
|
|
const {mode, host} = proxy["plugin-opts"];
|
|
obfs_opts = `,${mode},${host || ""}`;
|
|
} else {
|
|
throw new Error(
|
|
`Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}`
|
|
);
|
|
}
|
|
}
|
|
|
|
return `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"${obfs_opts}${udp_opts}`;
|
|
case "ssr":
|
|
return `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}",${proxy.protocol},{${proxy["protocol-param"] || ""}},${proxy.obfs},{${proxy["obfs-param"] || ""}}`;
|
|
case "vmess":
|
|
obfs_opts = "";
|
|
if (proxy.network === "ws") {
|
|
const host = proxy["ws-headers"].Host || proxy.server;
|
|
obfs_opts = `,transport:ws,host:${host},path:${
|
|
proxy["ws-path"] || "/"
|
|
}`;
|
|
} else {
|
|
obfs_opts = `,transport:tcp`;
|
|
}
|
|
if (proxy.tls) {
|
|
obfs_opts += `${
|
|
proxy.sni ? ",tls-name:" + proxy.sni : ""
|
|
},skip-cert-verify:${proxy['skip-cert-verify'] || "false"}`;
|
|
}
|
|
return `${proxy.name}=vmess,${proxy.server},${proxy.port},${
|
|
proxy.cipher === "auto" ? "none" : proxy.cipher
|
|
},"${proxy.uuid}",over-tls:${proxy.tls || "false"}${obfs_opts}`;
|
|
case "trojan":
|
|
return `${proxy.name}=trojan,${proxy.server},${proxy.port},"${
|
|
proxy.password
|
|
}"${
|
|
proxy.sni ? ",tls-name:" + proxy.sni : ""
|
|
},skip-cert-verify:${
|
|
proxy['skip-cert-verify'] || "false"
|
|
}`;
|
|
case "http":
|
|
tls_opts = "";
|
|
const base = `${proxy.name}=${proxy.tls ? "http" : "https"},${
|
|
proxy.server
|
|
},${proxy.port},${proxy.username || ""},${proxy.password || ""}`;
|
|
if (proxy.tls) {
|
|
// https
|
|
tls_opts = `${
|
|
proxy.sni ? ",tls-name:" + proxy.sni : ""
|
|
},skip-cert-verify:${proxy['skip-cert-verify']}`;
|
|
return base + tls_opts;
|
|
} else return base;
|
|
}
|
|
throw new Error(
|
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`
|
|
);
|
|
};
|
|
return {produce};
|
|
}
|
|
|
|
function Surge_Producer() {
|
|
const targetPlatform = "Surge";
|
|
const produce = (proxy) => {
|
|
let obfs_opts, tls_opts;
|
|
switch (proxy.type) {
|
|
case "ss":
|
|
obfs_opts = "";
|
|
if (proxy.plugin) {
|
|
const {host, mode} = proxy['plugin-opts'];
|
|
if (proxy.plugin === "obfs") {
|
|
obfs_opts = `,obfs=${mode}${
|
|
host ? ",obfs-host=" + host : ""
|
|
}`;
|
|
} else {
|
|
throw new Error(
|
|
`Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}`
|
|
);
|
|
}
|
|
}
|
|
return `${proxy.name}=ss,${proxy.server}, ${proxy.port},encrypt-method=${
|
|
proxy.cipher
|
|
},password=${proxy.password}${obfs_opts},tfo=${
|
|
proxy.tfo || "false"
|
|
},udp-relay=${proxy.udp || "false"}`;
|
|
case "vmess":
|
|
tls_opts = "";
|
|
let config = `${proxy.name}=vmess,${proxy.server},${
|
|
proxy.port
|
|
},username=${proxy.uuid},tls=${proxy.tls || "false"},tfo=${proxy.tfo || "false"}`;
|
|
if (proxy.network === "ws") {
|
|
const path = proxy["ws-path"] || "/";
|
|
const host = proxy["ws-headers"].Host;
|
|
config += `,ws=true${path ? ",ws-path=" + path : ""}${
|
|
host ? ",ws-headers=HOST:" + host : ""
|
|
}`;
|
|
}
|
|
if (proxy.tls) {
|
|
config += `${
|
|
typeof proxy['skip-cert-verify'] !== "undefined"
|
|
? ",skip-cert-verify=" + proxy['skip-cert-verify']
|
|
: ""
|
|
}`;
|
|
config += proxy.sni ? `,sni=${proxy.sni}` : "";
|
|
}
|
|
return config;
|
|
case "trojan":
|
|
return `${proxy.name}=trojan,${proxy.server},${proxy.port},password=${
|
|
proxy.password
|
|
}${
|
|
typeof proxy['skip-cert-verify'] !== "undefined"
|
|
? ",skip-cert-verify=" + proxy['skip-cert-verify']
|
|
: ""
|
|
}${proxy.sni ? ",sni=" + proxy.sni : ""},tfo=${proxy.tfo || "false"}`;
|
|
case "http":
|
|
tls_opts = ", tls=false";
|
|
if (proxy.tls) {
|
|
tls_opts = `,tls=true,skip-cert-verify=${proxy['skip-cert-verify']},sni=${proxy.sni}`;
|
|
}
|
|
return `${proxy.name}=http, ${proxy.server}, ${proxy.port}${
|
|
proxy.username ? ",username=" + proxy.username : ""
|
|
}${
|
|
proxy.password ? ",password=" + proxy.password : ""
|
|
}${tls_opts},tfo=${proxy.tfo || "false"}`;
|
|
}
|
|
throw new Error(
|
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`
|
|
);
|
|
};
|
|
return {produce};
|
|
}
|
|
|
|
function Clash_Producer() {
|
|
const type = "ALL";
|
|
const produce = (proxies) => {
|
|
return "proxies:\n" + proxies.map(proxy => {
|
|
delete proxy.supported;
|
|
return " - " + JSON.stringify(proxy) + "\n";
|
|
}).join("");
|
|
};
|
|
return {type, produce};
|
|
}
|
|
|
|
function URI_Producer() {
|
|
const type = "SINGLE";
|
|
const produce = (proxy) => {
|
|
let result = "";
|
|
switch (proxy.type) {
|
|
case "ss":
|
|
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
|
result = `ss://${Base64.safeEncode(userinfo)}@${proxy.server}:${
|
|
proxy.port
|
|
}/`;
|
|
if (proxy.plugin) {
|
|
result += "?plugin=";
|
|
const opts = proxy["plugin-opts"];
|
|
switch (proxy.plugin) {
|
|
case "obfs":
|
|
result += encodeURIComponent(
|
|
`simple-obfs;obfs=${opts.mode}${
|
|
opts.host ? ";obfs-host=" + opts.host : ""
|
|
}`
|
|
);
|
|
break;
|
|
case "v2ray-plugin":
|
|
result += encodeURIComponent(
|
|
`v2ray-plugin;obfs=${opts.mode}${
|
|
opts.host ? ";obfs-host" + opts.host : ""
|
|
}${opts.tls ? ";tls" : ""}`
|
|
);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported plugin option: ${proxy.plugin}`);
|
|
}
|
|
}
|
|
result += `#${encodeURIComponent(proxy.name)}`;
|
|
break;
|
|
case "ssr":
|
|
result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${
|
|
proxy.cipher
|
|
}:${proxy.obfs}:${Base64.safeEncode(proxy.password)}/`;
|
|
result += `?remarks=${Base64.safeEncode(proxy.name)}${
|
|
proxy["obfs-param"]
|
|
? "&obfsparam=" + Base64.safeEncode(proxy["obfs-param"])
|
|
: ""
|
|
}${
|
|
proxy["protocol-param"]
|
|
? "&protocolparam=" + Base64.safeEncode(proxy["protocol-param"])
|
|
: ""
|
|
}`;
|
|
result = "ssr://" + Base64.safeEncode(result);
|
|
break;
|
|
case "vmess":
|
|
// V2RayN URI format
|
|
result = {
|
|
ps: proxy.name,
|
|
add: proxy.server,
|
|
port: proxy.port,
|
|
id: proxy.uuid,
|
|
type: "",
|
|
aid: 0,
|
|
net: proxy.network || "tcp",
|
|
tls: proxy.tls ? "tls" : "",
|
|
};
|
|
// obfs
|
|
if (proxy.network === "ws") {
|
|
result.path = proxy["ws-path"] || "/";
|
|
result.host = proxy["ws-headers"].Host || proxy.server;
|
|
}
|
|
result = "vmess://" + Base64.safeEncode(JSON.stringify(result));
|
|
break;
|
|
case "trojan":
|
|
result = `trojan://${proxy.password}@${proxy.server}:${proxy.port}#${encodeURIComponent(proxy.name)}`;
|
|
break;
|
|
default:
|
|
throw new Error(`Cannot handle proxy type: ${proxy.type}`);
|
|
}
|
|
return result;
|
|
}
|
|
return {type, produce};
|
|
}
|
|
|
|
function JSON_Producer() {
|
|
const type = "ALL";
|
|
const produce = proxies => JSON.stringify(proxies, null, 2);
|
|
return {type, produce};
|
|
}
|
|
|
|
|
|
return {
|
|
"QX": QX_Producer(),
|
|
"Surge": Surge_Producer(),
|
|
"Loon": Loon_Producer(),
|
|
"Clash": Clash_Producer(),
|
|
"URI": URI_Producer(),
|
|
"JSON": JSON_Producer()
|
|
}
|
|
})();
|
|
|
|
function preprocess(raw) {
|
|
for (const processor of PROXY_PREPROCESSORS) {
|
|
try {
|
|
if (processor.test(raw)) {
|
|
$.info(`Pre-processor [${processor.name}] activated`);
|
|
return processor.parse(raw);
|
|
}
|
|
} catch (e) {
|
|
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
|
|
}
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function safeMatch(p, line) {
|
|
let patternMatched;
|
|
try {
|
|
patternMatched = p.test(line);
|
|
} catch (err) {
|
|
patternMatched = false;
|
|
}
|
|
return patternMatched;
|
|
}
|
|
|
|
function parse(raw) {
|
|
raw = preprocess(raw);
|
|
// parse
|
|
const lines = raw.split("\n");
|
|
const proxies = [];
|
|
let lastParser;
|
|
|
|
for (let line of lines) {
|
|
line = line.trim();
|
|
if (line.length === 0) continue; // skip empty line
|
|
let matched = lastParser && safeMatch(lastParser, line);
|
|
if (!matched) {
|
|
for (const parser of PROXY_PARSERS) {
|
|
if (safeMatch(parser, line)) {
|
|
lastParser = parser;
|
|
matched = true;
|
|
$.info(`Proxy parser: ${parser.name} is activated`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!matched) {
|
|
$.error(`Failed to find a rule to parse line: \n${line}\n`);
|
|
} else {
|
|
try {
|
|
const proxy = lastParser.parse(line);
|
|
if (!proxy) {
|
|
$.error(`Parser ${lastParser.name} return nothing for \n${line}\n`);
|
|
}
|
|
proxies.push(proxy);
|
|
} catch (err) {
|
|
$.error(
|
|
`Failed to parse line: \n ${line}\n Reason: ${err.stack}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return proxies;
|
|
}
|
|
|
|
async function process(proxies, operators = []) {
|
|
for (const item of operators) {
|
|
// process script
|
|
let script;
|
|
if (item.type.indexOf("Script") !== -1) {
|
|
const {mode, content} = item.args;
|
|
if (mode === "link") {
|
|
// if this is remote script, download it
|
|
try {
|
|
script = await $.http
|
|
.get(content)
|
|
.then((resp) => resp.body);
|
|
} catch (err) {
|
|
$.error(
|
|
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`
|
|
);
|
|
// skip the script if download failed.
|
|
continue;
|
|
}
|
|
|
|
} else {
|
|
script = content;
|
|
}
|
|
}
|
|
|
|
if (!PROXY_PROCESSORS[item.type]) {
|
|
$.error(`Unknown operator: "${item.type}"`);
|
|
continue;
|
|
}
|
|
|
|
$.info(
|
|
`Applying "${item.type}" with arguments:\n >>> ${
|
|
JSON.stringify(item.args, null, 2) || "None"
|
|
}`
|
|
);
|
|
let processor;
|
|
if (item.type.indexOf('Script') !== -1) {
|
|
processor = PROXY_PROCESSORS[item.type](script);
|
|
} else {
|
|
processor = PROXY_PROCESSORS[item.type](item.args);
|
|
}
|
|
proxies = ApplyProcessor(processor, proxies);
|
|
}
|
|
return proxies;
|
|
}
|
|
|
|
function produce(proxies, targetPlatform) {
|
|
const producer = PROXY_PRODUCERS[targetPlatform];
|
|
if (!producer) {
|
|
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
|
}
|
|
|
|
// filter unsupported proxies
|
|
proxies = proxies.filter(proxy => !(proxy.supported && proxy.supported[targetPlatform] === false));
|
|
|
|
$.info(`Producing proxies for target: ${targetPlatform}`);
|
|
if (typeof producer.type === "undefined" || producer.type === 'SINGLE') {
|
|
return proxies
|
|
.map(proxy => {
|
|
try {
|
|
return producer.produce(proxy);
|
|
} catch (err) {
|
|
$.error(
|
|
`Cannot produce proxy: ${JSON.stringify(
|
|
proxy, null, 2
|
|
)}\nReason: ${err}`
|
|
);
|
|
return "";
|
|
}
|
|
})
|
|
.filter(line => line.length > 0)
|
|
.join("\n");
|
|
} else if (producer.type === "ALL") {
|
|
return producer.produce(proxies);
|
|
}
|
|
}
|
|
|
|
return {
|
|
parse, process, produce
|
|
}
|
|
})();
|
|
|
|
/****************************************** Rule Utils **********************************************************/
|
|
var RuleUtils = (function () {
|
|
const RULE_TYPES_MAPPING = [
|
|
[/^(DOMAIN|host|HOST)$/, "DOMAIN"],
|
|
[/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, "DOMAIN-KEYWORD"],
|
|
[/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, "DOMAIN-SUFFIX"],
|
|
[/^USER-AGENT$/i, "USER-AGENT"],
|
|
[/^PROCESS-NAME$/, "PROCESS-NAME"],
|
|
[/^(DEST-PORT|DST-PORT)$/, "DST-PORT"],
|
|
[/^SRC-IP(-CIDR)?$/, "SRC-IP"],
|
|
[/^(IN|SRC)-PORT$/, "IN-PORT"],
|
|
[/^PROTOCOL$/, "PROTOCOL"],
|
|
[/^IP-CIDR$/i, "IP-CIDR"],
|
|
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/]
|
|
];
|
|
|
|
const RULE_PREPROCESSORS = (function () {
|
|
function HTML() {
|
|
const name = "HTML";
|
|
const test = raw => /^<!DOCTYPE html>/.test(raw);
|
|
// simply discard HTML
|
|
const parse = _ => "";
|
|
return {name, test, parse};
|
|
}
|
|
|
|
function ClashProvider() {
|
|
const name = "Clash Provider";
|
|
const test = raw => raw.indexOf("payload:") === 0
|
|
const parse = raw => {
|
|
return raw
|
|
.replace("payload:", "")
|
|
.replace(/^\s*-\s*/gm, "");
|
|
}
|
|
return {name, test, parse}
|
|
}
|
|
|
|
return [HTML(), ClashProvider()];
|
|
})();
|
|
const RULE_PARSERS = (function () {
|
|
function AllRuleParser() {
|
|
const name = "Universal Rule Parser";
|
|
const test = () => true;
|
|
const parse = (raw) => {
|
|
const lines = raw.split("\n");
|
|
const result = [];
|
|
for (let line of lines) {
|
|
line = line.trim();
|
|
// skip empty line
|
|
if (line.length === 0) continue;
|
|
// skip comments
|
|
if (/\s*#/.test(line)) continue;
|
|
try {
|
|
const params = line.split(",").map(w => w.trim());
|
|
let rawType = params[0];
|
|
let matched = false;
|
|
for (const item of RULE_TYPES_MAPPING) {
|
|
const regex = item[0];
|
|
if (regex.test(rawType)) {
|
|
matched = true;
|
|
const rule = {
|
|
type: item[1],
|
|
content: params[1],
|
|
};
|
|
if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") {
|
|
rule.options = params.slice(2)
|
|
}
|
|
result.push(rule);
|
|
}
|
|
}
|
|
if (!matched) throw new Error("Invalid rule type: " + rawType);
|
|
} catch (e) {
|
|
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
return {name, test, parse};
|
|
}
|
|
|
|
return [AllRuleParser()];
|
|
})();
|
|
const RULE_PROCESSORS = (function () {
|
|
function RegexFilter({regex = [], keep = true}) {
|
|
return {
|
|
name: "Regex Filter",
|
|
func: (rules) => {
|
|
return rules.map((rule) => {
|
|
const selected = regex.some((r) => {
|
|
r = new RegExp(r);
|
|
return r.test(rule);
|
|
});
|
|
return keep ? selected : !selected;
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
function TypeFilter(types) {
|
|
return {
|
|
name: "Type Filter",
|
|
func: (rules) => {
|
|
return rules.map((rule) => types.some((t) => rule.type === t));
|
|
},
|
|
};
|
|
}
|
|
|
|
function RemoveDuplicateFilter() {
|
|
return {
|
|
name: "Remove Duplicate Filter",
|
|
func: rules => {
|
|
const seen = new Set();
|
|
const result = [];
|
|
rules.forEach(rule => {
|
|
const options = rule.options || [];
|
|
options.sort();
|
|
const key = `${rule.type},${rule.content},${JSON.stringify(options)}`;
|
|
if (!seen.has(key)) {
|
|
result.push(rule)
|
|
seen.add(key);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// regex: [{expr: "string format regex", now: "now"}]
|
|
function RegexReplaceOperator(regex) {
|
|
return {
|
|
name: "Regex Rename Operator",
|
|
func: (rules) => {
|
|
return rules.map((rule) => {
|
|
for (const {expr, now} of regex) {
|
|
rule.content = rule.content.replace(new RegExp(expr, "g"), now).trim();
|
|
}
|
|
return rule;
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
"Regex Filter": RegexFilter,
|
|
"Remove Duplicate Filter": RemoveDuplicateFilter,
|
|
"Type Filter": TypeFilter,
|
|
|
|
"Regex Replace Operator": RegexReplaceOperator
|
|
};
|
|
})();
|
|
const RULE_PRODUCERS = (function () {
|
|
function QXFilter() {
|
|
const type = "SINGLE";
|
|
const func = (rule) => {
|
|
// skip unsupported rules
|
|
const UNSUPPORTED = [
|
|
"URL-REGEX", "DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL"
|
|
];
|
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
|
|
|
const TRANSFORM = {
|
|
"DOMAIN-KEYWORD": "HOST-KEYWORD",
|
|
"DOMAIN-SUFFIX": "HOST-SUFFIX",
|
|
"DOMAIN": "HOST",
|
|
"IP-CIDR6": "IP6-CIDR"
|
|
};
|
|
|
|
// QX does not support the no-resolve option
|
|
return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`;
|
|
}
|
|
return {type, func};
|
|
}
|
|
|
|
function SurgeRuleSet() {
|
|
const type = "SINGLE";
|
|
const func = (rule) => {
|
|
let output = `${rule.type},${rule.content}`;
|
|
if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") {
|
|
output += rule.options ? `,${rule.options[0]}` : "";
|
|
}
|
|
return output;
|
|
}
|
|
return {type, func};
|
|
}
|
|
|
|
function LoonRules() {
|
|
const type = "SINGLE";
|
|
const func = (rule) => {
|
|
// skip unsupported rules
|
|
const UNSUPPORTED = [
|
|
"DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL"
|
|
];
|
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
|
return SurgeRuleSet().func(rule);
|
|
}
|
|
return {type, func};
|
|
}
|
|
|
|
function ClashRuleProvider() {
|
|
const type = "ALL";
|
|
const func = (rules) => {
|
|
const TRANSFORM = {
|
|
"DEST-PORT": "DST-PORT",
|
|
"SRC-IP": "SRC-IP-CIDR",
|
|
"IN-PORT": "SRC-PORT"
|
|
};
|
|
const conf = {
|
|
payload: rules.map(rule => {
|
|
let output = `${TRANSFORM[rule.type] || rule.type},${rule.content}`;
|
|
if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") {
|
|
output += rule.options ? `,${rule.options[0]}` : "";
|
|
}
|
|
return output;
|
|
})
|
|
}
|
|
return YAML.stringify(conf);
|
|
}
|
|
return {type, func};
|
|
}
|
|
|
|
return {
|
|
"QX": QXFilter(),
|
|
"Surge": SurgeRuleSet(),
|
|
"Loon": LoonRules(),
|
|
"Clash": ClashRuleProvider()
|
|
};
|
|
})();
|
|
|
|
function preprocess(raw) {
|
|
for (const processor of RULE_PREPROCESSORS) {
|
|
try {
|
|
if (processor.test(raw)) {
|
|
$.info(`Pre-processor [${processor.name}] activated`);
|
|
return processor.parse(raw);
|
|
}
|
|
} catch (e) {
|
|
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
|
|
}
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function parse(raw) {
|
|
raw = preprocess(raw);
|
|
for (const parser of RULE_PARSERS) {
|
|
let matched;
|
|
try {
|
|
matched = parser.test(raw);
|
|
} catch {
|
|
matched = false;
|
|
}
|
|
if (matched) {
|
|
$.info(`Rule parser [${parser.name}] is activated!`);
|
|
return parser.parse(raw);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function process(rules, operators) {
|
|
for (const item of operators) {
|
|
if (!RULE_PROCESSORS[item.type]) {
|
|
console.error(`Unknown operator: ${item.type}!`);
|
|
continue;
|
|
}
|
|
const processor = RULE_PROCESSORS[item.type](item.args);
|
|
$.info(
|
|
`Applying "${item.type}" with arguments: \n >>> ${
|
|
JSON.stringify(item.args) || "None"
|
|
}`
|
|
);
|
|
rules = ApplyProcessor(processor, rules);
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
function produce(rules, targetPlatform) {
|
|
const producer = RULE_PRODUCERS[targetPlatform];
|
|
if (!producer) {
|
|
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
|
}
|
|
if (typeof producer.type === "undefined" || producer.type === 'SINGLE') {
|
|
return rules
|
|
.map(rule => {
|
|
try {
|
|
return producer.func(rule);
|
|
} catch (err) {
|
|
console.log(
|
|
`ERROR: cannot produce rule: ${JSON.stringify(
|
|
rule
|
|
)}\nReason: ${err}`
|
|
);
|
|
return "";
|
|
}
|
|
})
|
|
.filter(line => line.length > 0)
|
|
.join("\n");
|
|
} else if (producer.type === "ALL") {
|
|
return producer.func(rules);
|
|
}
|
|
}
|
|
|
|
return {parse, process, produce};
|
|
})();
|
|
|
|
parseResource();
|
|
/****************************************** Supporting Functions ********************************************** */
|
|
function ApplyProcessor(process, objs) {
|
|
function ApplyFilter(filter, objs) {
|
|
// select proxies
|
|
let selected = FULL(objs.length, true);
|
|
try {
|
|
selected = AND(selected, filter.func(objs));
|
|
} catch (err) {
|
|
// print log and skip this filter
|
|
console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
|
}
|
|
return objs.filter((_, i) => selected[i]);
|
|
}
|
|
|
|
function ApplyOperator(operator, objs) {
|
|
let output = clone(objs);
|
|
try {
|
|
const output_ = operator.func(output);
|
|
if (output_) output = output_;
|
|
} catch (err) {
|
|
// print log and skip this operator
|
|
console.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
if (process.name.indexOf("Filter") !== -1) {
|
|
return ApplyFilter(process, objs);
|
|
} else if (process.name.indexOf("Operator") !== -1) {
|
|
return ApplyOperator(process, objs);
|
|
}
|
|
|
|
}
|
|
|
|
// some logical functions
|
|
function AND(...args) {
|
|
return args.reduce((a, b) => a.map((c, i) => b[i] && c));
|
|
}
|
|
|
|
function FULL(length, bool) {
|
|
return [...Array(length).keys()].map(() => bool);
|
|
}
|
|
|
|
function clone(object) {
|
|
return JSON.parse(JSON.stringify(object));
|
|
}
|
|
|
|
/****************************************** Own Libraries *******************************************************/
|
|
|
|
|
|
/**
|
|
* OpenAPI
|
|
* https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md
|
|
*/
|
|
function ENV() {
|
|
const isQX = typeof $task !== "undefined";
|
|
const isLoon = typeof $loon !== "undefined";
|
|
const isSurge = typeof $httpClient !== "undefined" && !isLoon;
|
|
const isJSBox = typeof require == "function" && typeof $jsbox != "undefined";
|
|
const isNode = typeof require == "function" && !isJSBox;
|
|
const isRequest = typeof $request !== "undefined";
|
|
const isScriptable = typeof importModule !== "undefined";
|
|
return {isQX, isLoon, isSurge, isNode, isJSBox, isRequest, isScriptable};
|
|
}
|
|
|
|
function HTTP(defaultOptions = {baseURL: ""}) {
|
|
const {isQX, isLoon, isSurge, isScriptable, isNode} = ENV();
|
|
const methods = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"];
|
|
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/
|
|
|
|
function send(method, options) {
|
|
options = typeof options === "string" ? {url: options} : options;
|
|
const baseURL = defaultOptions.baseURL;
|
|
if (baseURL && !URL_REGEX.test(options.url || "")) {
|
|
options.url = baseURL ? baseURL + options.url : options.url;
|
|
}
|
|
options = {...defaultOptions, ...options};
|
|
const timeout = options.timeout;
|
|
const events = {
|
|
...{
|
|
onRequest: () => {
|
|
},
|
|
onResponse: (resp) => resp,
|
|
onTimeout: () => {
|
|
},
|
|
},
|
|
...options.events,
|
|
};
|
|
|
|
events.onRequest(method, options);
|
|
|
|
let worker;
|
|
if (isQX) {
|
|
worker = $task.fetch({method, ...options});
|
|
} else if (isLoon || isSurge || isNode) {
|
|
worker = new Promise((resolve, reject) => {
|
|
const request = isNode ? require("request") : $httpClient;
|
|
request[method.toLowerCase()](options, (err, response, body) => {
|
|
if (err) reject(err);
|
|
else
|
|
resolve({
|
|
statusCode: response.status || response.statusCode,
|
|
headers: response.headers,
|
|
body,
|
|
});
|
|
});
|
|
});
|
|
} else if (isScriptable) {
|
|
const request = new Request(options.url);
|
|
request.method = method;
|
|
request.headers = options.headers;
|
|
request.body = options.body;
|
|
worker = new Promise((resolve, reject) => {
|
|
request
|
|
.loadString()
|
|
.then((body) => {
|
|
resolve({
|
|
statusCode: request.response.statusCode,
|
|
headers: request.response.headers,
|
|
body,
|
|
});
|
|
})
|
|
.catch((err) => reject(err));
|
|
});
|
|
}
|
|
|
|
let timeoutid;
|
|
const timer = timeout
|
|
? new Promise((_, reject) => {
|
|
timeoutid = setTimeout(() => {
|
|
events.onTimeout();
|
|
return reject(
|
|
`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`
|
|
);
|
|
}, timeout);
|
|
})
|
|
: null;
|
|
|
|
return (timer
|
|
? Promise.race([timer, worker]).then((res) => {
|
|
clearTimeout(timeoutid);
|
|
return res;
|
|
})
|
|
: worker
|
|
).then((resp) => events.onResponse(resp));
|
|
}
|
|
|
|
const http = {};
|
|
methods.forEach(
|
|
(method) =>
|
|
(http[method.toLowerCase()] = (options) => send(method, options))
|
|
);
|
|
return http;
|
|
}
|
|
|
|
function API(name = "untitled", debug = false) {
|
|
const {isQX, isLoon, isSurge, isNode, isJSBox, isScriptable} = ENV();
|
|
return new (class {
|
|
constructor(name, debug) {
|
|
this.name = name;
|
|
this.debug = debug;
|
|
|
|
this.http = HTTP();
|
|
this.env = ENV();
|
|
|
|
this.node = (() => {
|
|
if (isNode) {
|
|
const fs = require("fs");
|
|
|
|
return {
|
|
fs,
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
})();
|
|
this.initCache();
|
|
|
|
const delay = (t, v) =>
|
|
new Promise(function (resolve) {
|
|
setTimeout(resolve.bind(null, v), t);
|
|
});
|
|
|
|
Promise.prototype.delay = function (t) {
|
|
return this.then(function (v) {
|
|
return delay(t, v);
|
|
});
|
|
};
|
|
}
|
|
|
|
// persistence
|
|
// initialize cache
|
|
initCache() {
|
|
if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}");
|
|
if (isLoon || isSurge)
|
|
this.cache = JSON.parse($persistentStore.read(this.name) || "{}");
|
|
|
|
if (isNode) {
|
|
// create a json for root cache
|
|
let fpath = "root.json";
|
|
if (!this.node.fs.existsSync(fpath)) {
|
|
this.node.fs.writeFileSync(
|
|
fpath,
|
|
JSON.stringify({}),
|
|
{flag: "wx"},
|
|
(err) => console.log(err)
|
|
);
|
|
}
|
|
this.root = {};
|
|
|
|
// create a json file with the given name if not exists
|
|
fpath = `${this.name}.json`;
|
|
if (!this.node.fs.existsSync(fpath)) {
|
|
this.node.fs.writeFileSync(
|
|
fpath,
|
|
JSON.stringify({}),
|
|
{flag: "wx"},
|
|
(err) => console.log(err)
|
|
);
|
|
this.cache = {};
|
|
} else {
|
|
this.cache = JSON.parse(
|
|
this.node.fs.readFileSync(`${this.name}.json`)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// store cache
|
|
persistCache() {
|
|
const data = JSON.stringify(this.cache, null, 2);
|
|
if (isQX) $prefs.setValueForKey(data, this.name);
|
|
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
|
if (isNode) {
|
|
this.node.fs.writeFileSync(
|
|
`${this.name}.json`,
|
|
data,
|
|
{flag: "w"},
|
|
(err) => console.log(err)
|
|
);
|
|
this.node.fs.writeFileSync(
|
|
"root.json",
|
|
JSON.stringify(this.root, null, 2),
|
|
{flag: "w"},
|
|
(err) => console.log(err)
|
|
);
|
|
}
|
|
}
|
|
|
|
write(data, key) {
|
|
this.log(`SET ${key}`);
|
|
if (key.indexOf("#") !== -1) {
|
|
key = key.substr(1);
|
|
if (isSurge || isLoon) {
|
|
return $persistentStore.write(data, key);
|
|
}
|
|
if (isQX) {
|
|
return $prefs.setValueForKey(data, key);
|
|
}
|
|
if (isNode) {
|
|
this.root[key] = data;
|
|
}
|
|
} else {
|
|
this.cache[key] = data;
|
|
}
|
|
this.persistCache();
|
|
}
|
|
|
|
read(key) {
|
|
this.log(`READ ${key}`);
|
|
if (key.indexOf("#") !== -1) {
|
|
key = key.substr(1);
|
|
if (isSurge || isLoon) {
|
|
return $persistentStore.read(key);
|
|
}
|
|
if (isQX) {
|
|
return $prefs.valueForKey(key);
|
|
}
|
|
if (isNode) {
|
|
return this.root[key];
|
|
}
|
|
} else {
|
|
return this.cache[key];
|
|
}
|
|
}
|
|
|
|
delete(key) {
|
|
this.log(`DELETE ${key}`);
|
|
if (key.indexOf("#") !== -1) {
|
|
key = key.substr(1);
|
|
if (isSurge || isLoon) {
|
|
return $persistentStore.write(null, key);
|
|
}
|
|
if (isQX) {
|
|
return $prefs.removeValueForKey(key);
|
|
}
|
|
if (isNode) {
|
|
delete this.root[key];
|
|
}
|
|
} else {
|
|
delete this.cache[key];
|
|
}
|
|
this.persistCache();
|
|
}
|
|
|
|
// notification
|
|
notify(title, subtitle = "", content = "", options = {}) {
|
|
const openURL = options["open-url"];
|
|
const mediaURL = options["media-url"];
|
|
|
|
if (isQX) $notify(title, subtitle, content, options);
|
|
if (isSurge) {
|
|
$notification.post(
|
|
title,
|
|
subtitle,
|
|
content + `${mediaURL ? "\n多媒体:" + mediaURL : ""}`,
|
|
{
|
|
url: openURL,
|
|
}
|
|
);
|
|
}
|
|
if (isLoon) {
|
|
let opts = {};
|
|
if (openURL) opts["openUrl"] = openURL;
|
|
if (mediaURL) opts["mediaUrl"] = mediaURL;
|
|
if (JSON.stringify(opts) === "{}") {
|
|
$notification.post(title, subtitle, content);
|
|
} else {
|
|
$notification.post(title, subtitle, content, opts);
|
|
}
|
|
}
|
|
if (isNode || isScriptable) {
|
|
const content_ =
|
|
content +
|
|
(openURL ? `\n点击跳转: ${openURL}` : "") +
|
|
(mediaURL ? `\n多媒体: ${mediaURL}` : "");
|
|
if (isJSBox) {
|
|
const push = require("push");
|
|
push.schedule({
|
|
title: title,
|
|
body: (subtitle ? subtitle + "\n" : "") + content_,
|
|
});
|
|
} else {
|
|
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// other helper functions
|
|
log(msg) {
|
|
if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);
|
|
}
|
|
|
|
info(msg) {
|
|
console.log(`[${this.name}] INFO: ${msg}`);
|
|
}
|
|
|
|
error(msg) {
|
|
console.log(`[${this.name}] ERROR: ${msg}`);
|
|
}
|
|
|
|
wait(millisec) {
|
|
return new Promise((resolve) => setTimeout(resolve, millisec));
|
|
}
|
|
|
|
done(value = {}) {
|
|
if (isQX || isLoon || isSurge) {
|
|
$done(value);
|
|
} else if (isNode && !isJSBox) {
|
|
if (typeof $context !== "undefined") {
|
|
$context.headers = value.headers;
|
|
$context.statusCode = value.statusCode;
|
|
$context.body = value.body;
|
|
}
|
|
}
|
|
}
|
|
})(name, debug);
|
|
}
|
|
|
|
/****************************************** Third Party Libraries **********************************************/
|
|
|
|
/**
|
|
* Base64 Coding Library
|
|
* https://github.com/dankogai/js-base64#readme
|
|
*/
|
|
function Base64Code() {
|
|
// constants
|
|
const b64chars =
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
const b64tab = (function (bin) {
|
|
const t = {};
|
|
let i = 0;
|
|
const l = bin.length;
|
|
for (; i < l; i++) t[bin.charAt(i)] = i;
|
|
return t;
|
|
})(b64chars);
|
|
const fromCharCode = String.fromCharCode;
|
|
// encoder stuff
|
|
const cb_utob = function (c) {
|
|
let cc;
|
|
if (c.length < 2) {
|
|
cc = c.charCodeAt(0);
|
|
return cc < 0x80
|
|
? c
|
|
: cc < 0x800
|
|
? fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f))
|
|
: fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
|
|
fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
|
|
fromCharCode(0x80 | (cc & 0x3f));
|
|
} else {
|
|
cc =
|
|
0x10000 +
|
|
(c.charCodeAt(0) - 0xd800) * 0x400 +
|
|
(c.charCodeAt(1) - 0xdc00);
|
|
return (
|
|
fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) +
|
|
fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) +
|
|
fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
|
|
fromCharCode(0x80 | (cc & 0x3f))
|
|
);
|
|
}
|
|
};
|
|
const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
|
|
const utob = function (u) {
|
|
return u.replace(re_utob, cb_utob);
|
|
};
|
|
const cb_encode = function (ccc) {
|
|
const padlen = [0, 2, 1][ccc.length % 3],
|
|
ord =
|
|
(ccc.charCodeAt(0) << 16) |
|
|
((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8) |
|
|
(ccc.length > 2 ? ccc.charCodeAt(2) : 0),
|
|
chars = [
|
|
b64chars.charAt(ord >>> 18),
|
|
b64chars.charAt((ord >>> 12) & 63),
|
|
padlen >= 2 ? "=" : b64chars.charAt((ord >>> 6) & 63),
|
|
padlen >= 1 ? "=" : b64chars.charAt(ord & 63),
|
|
];
|
|
return chars.join("");
|
|
};
|
|
const btoa = function (b) {
|
|
return b.replace(/[\s\S]{1,3}/g, cb_encode);
|
|
};
|
|
this.encode = function (u) {
|
|
const isUint8Array =
|
|
Object.prototype.toString.call(u) === "[object Uint8Array]";
|
|
return isUint8Array ? u.toString("base64") : btoa(utob(String(u)));
|
|
};
|
|
const uriencode = function (u, urisafe) {
|
|
return !urisafe
|
|
? _encode(u)
|
|
: _encode(String(u))
|
|
.replace(/[+\/]/g, function (m0) {
|
|
return m0 === "+" ? "-" : "_";
|
|
})
|
|
.replace(/=/g, "");
|
|
};
|
|
const encodeURI = function (u) {
|
|
return uriencode(u, true);
|
|
};
|
|
// decoder stuff
|
|
const re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
|
|
const cb_btou = function (cccc) {
|
|
switch (cccc.length) {
|
|
case 4:
|
|
const cp =
|
|
((0x07 & cccc.charCodeAt(0)) << 18) |
|
|
((0x3f & cccc.charCodeAt(1)) << 12) |
|
|
((0x3f & cccc.charCodeAt(2)) << 6) |
|
|
(0x3f & cccc.charCodeAt(3)),
|
|
offset = cp - 0x10000;
|
|
return (
|
|
fromCharCode((offset >>> 10) + 0xd800) +
|
|
fromCharCode((offset & 0x3ff) + 0xdc00)
|
|
);
|
|
case 3:
|
|
return fromCharCode(
|
|
((0x0f & cccc.charCodeAt(0)) << 12) |
|
|
((0x3f & cccc.charCodeAt(1)) << 6) |
|
|
(0x3f & cccc.charCodeAt(2))
|
|
);
|
|
default:
|
|
return fromCharCode(
|
|
((0x1f & cccc.charCodeAt(0)) << 6) | (0x3f & cccc.charCodeAt(1))
|
|
);
|
|
}
|
|
};
|
|
const btou = function (b) {
|
|
return b.replace(re_btou, cb_btou);
|
|
};
|
|
const cb_decode = function (cccc) {
|
|
const len = cccc.length,
|
|
padlen = len % 4,
|
|
n =
|
|
(len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) |
|
|
(len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) |
|
|
(len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) |
|
|
(len > 3 ? b64tab[cccc.charAt(3)] : 0),
|
|
chars = [
|
|
fromCharCode(n >>> 16),
|
|
fromCharCode((n >>> 8) & 0xff),
|
|
fromCharCode(n & 0xff),
|
|
];
|
|
chars.length -= [0, 0, 2, 1][padlen];
|
|
return chars.join("");
|
|
};
|
|
const _atob = function (a) {
|
|
return a.replace(/\S{1,4}/g, cb_decode);
|
|
};
|
|
const atob = function (a) {
|
|
return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ""));
|
|
};
|
|
const _decode = function (u) {
|
|
return btou(_atob(u));
|
|
};
|
|
this.decode = function (a) {
|
|
return _decode(
|
|
String(a)
|
|
.replace(/[-_]/g, function (m0) {
|
|
return m0 === "-" ? "+" : "/";
|
|
})
|
|
.replace(/[^A-Za-z0-9\+\/]/g, "")
|
|
)
|
|
.replace(/>/g, ">")
|
|
.replace(/</g, "<");
|
|
};
|
|
this.safeEncode = function (a) {
|
|
return this.encode(a.replace(/\+/g, "-").replace(/\//g, "_"));
|
|
};
|
|
this.safeDecode = function (a) {
|
|
return this.decode(a.replace(/-/g, "+").replace(/_/g, "/"));
|
|
};
|
|
}
|
|
|
|
/**
|
|
* YAML parser for Javascript
|
|
* Author: Diogo Costa
|
|
*/
|
|
var YAML = (function () {
|
|
var errors = [],
|
|
reference_blocks = [],
|
|
processing_time = 0,
|
|
regex = {
|
|
regLevel: new RegExp("^([\\s\\-]+)"),
|
|
invalidLine: new RegExp("^\\-\\-\\-|^\\.\\.\\.|^\\s*#.*|^\\s*$"),
|
|
dashesString: new RegExp('^\\s*\\"([^\\"]*)\\"\\s*$'),
|
|
quotesString: new RegExp("^\\s*\\'([^\\']*)\\'\\s*$"),
|
|
float: new RegExp("^[+-]?[0-9]+\\.[0-9]+(e[+-]?[0-9]+(\\.[0-9]+)?)?$"),
|
|
integer: new RegExp("^[+-]?[0-9]+$"),
|
|
array: new RegExp("\\[\\s*(.*)\\s*\\]"),
|
|
map: new RegExp("\\{\\s*(.*)\\s*\\}"),
|
|
key_value: new RegExp("([a-z0-9_-][ a-z0-9_-]*):( .+)", "i"),
|
|
single_key_value: new RegExp("^([a-z0-9_-][ a-z0-9_-]*):( .+?)$", "i"),
|
|
key: new RegExp("([a-z0-9_-][ a-z0-9_-]*):( .+)?", "i"),
|
|
item: new RegExp("^-\\s+"),
|
|
trim: new RegExp("^\\s+|\\s+$"),
|
|
comment: new RegExp(
|
|
"([^\\'\\\"#]+([\\'\\\"][^\\'\\\"]*[\\'\\\"])*)*(#.*)?"
|
|
),
|
|
};
|
|
|
|
/**
|
|
* @class A block of lines of a given level.
|
|
* @param {int} lvl The block's level.
|
|
* @private
|
|
*/
|
|
function Block(lvl) {
|
|
return {
|
|
/* The block's parent */
|
|
parent: null,
|
|
/* Number of children */
|
|
length: 0,
|
|
/* Block's level */
|
|
level: lvl,
|
|
/* Lines of code to process */
|
|
lines: [],
|
|
/* Blocks with greater level */
|
|
children: [],
|
|
/* Add a block to the children collection */
|
|
addChild: function (obj) {
|
|
this.children.push(obj);
|
|
obj.parent = this;
|
|
++this.length;
|
|
},
|
|
};
|
|
}
|
|
|
|
function parser(str) {
|
|
var regLevel = regex["regLevel"];
|
|
var invalidLine = regex["invalidLine"];
|
|
var lines = str.split("\n");
|
|
var m;
|
|
var level = 0,
|
|
curLevel = 0;
|
|
|
|
var blocks = [];
|
|
|
|
var result = new Block(-1);
|
|
var currentBlock = new Block(0);
|
|
result.addChild(currentBlock);
|
|
var levels = [];
|
|
var line = "";
|
|
|
|
blocks.push(currentBlock);
|
|
levels.push(level);
|
|
|
|
for (var i = 0, len = lines.length; i < len; ++i) {
|
|
line = lines[i];
|
|
|
|
if (line.match(invalidLine)) {
|
|
continue;
|
|
}
|
|
|
|
if ((m = regLevel.exec(line))) {
|
|
level = m[1].length;
|
|
} else level = 0;
|
|
|
|
if (level > curLevel) {
|
|
var oldBlock = currentBlock;
|
|
currentBlock = new Block(level);
|
|
oldBlock.addChild(currentBlock);
|
|
blocks.push(currentBlock);
|
|
levels.push(level);
|
|
} else if (level < curLevel) {
|
|
var added = false;
|
|
|
|
var k = levels.length - 1;
|
|
for (; k >= 0; --k) {
|
|
if (levels[k] == level) {
|
|
currentBlock = new Block(level);
|
|
blocks.push(currentBlock);
|
|
levels.push(level);
|
|
if (blocks[k].parent != null)
|
|
blocks[k].parent.addChild(currentBlock);
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!added) {
|
|
errors.push("Error: Invalid indentation at line " + i + ": " + line);
|
|
return;
|
|
}
|
|
}
|
|
|
|
currentBlock.lines.push(line.replace(regex["trim"], ""));
|
|
curLevel = level;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function processValue(val) {
|
|
val = val.replace(regex["trim"], "");
|
|
var m = null;
|
|
|
|
if (val == "true") {
|
|
return true;
|
|
} else if (val == "false") {
|
|
return false;
|
|
} else if (val == ".NaN") {
|
|
return Number.NaN;
|
|
} else if (val == "null") {
|
|
return null;
|
|
} else if (val == ".inf") {
|
|
return Number.POSITIVE_INFINITY;
|
|
} else if (val == "-.inf") {
|
|
return Number.NEGATIVE_INFINITY;
|
|
} else if ((m = val.match(regex["dashesString"]))) {
|
|
return m[1];
|
|
} else if ((m = val.match(regex["quotesString"]))) {
|
|
return m[1];
|
|
} else if ((m = val.match(regex["float"]))) {
|
|
return parseFloat(m[0]);
|
|
} else if ((m = val.match(regex["integer"]))) {
|
|
return parseInt(m[0]);
|
|
} else if (!isNaN((m = Date.parse(val)))) {
|
|
return new Date(m);
|
|
} else if ((m = val.match(regex["single_key_value"]))) {
|
|
var res = {};
|
|
res[m[1]] = processValue(m[2]);
|
|
return res;
|
|
} else if ((m = val.match(regex["array"]))) {
|
|
var count = 0,
|
|
c = " ";
|
|
var res = [];
|
|
var content = "";
|
|
var str = false;
|
|
for (var j = 0, lenJ = m[1].length; j < lenJ; ++j) {
|
|
c = m[1][j];
|
|
if (c == "'" || c == '"') {
|
|
if (str === false) {
|
|
str = c;
|
|
content += c;
|
|
continue;
|
|
} else if ((c == "'" && str == "'") || (c == '"' && str == '"')) {
|
|
str = false;
|
|
content += c;
|
|
continue;
|
|
}
|
|
} else if (str === false && (c == "[" || c == "{")) {
|
|
++count;
|
|
} else if (str === false && (c == "]" || c == "}")) {
|
|
--count;
|
|
} else if (str === false && count == 0 && c == ",") {
|
|
res.push(processValue(content));
|
|
content = "";
|
|
continue;
|
|
}
|
|
|
|
content += c;
|
|
}
|
|
|
|
if (content.length > 0) res.push(processValue(content));
|
|
return res;
|
|
} else if ((m = val.match(regex["map"]))) {
|
|
var count = 0,
|
|
c = " ";
|
|
var res = [];
|
|
var content = "";
|
|
var str = false;
|
|
for (var j = 0, lenJ = m[1].length; j < lenJ; ++j) {
|
|
c = m[1][j];
|
|
if (c == "'" || c == '"') {
|
|
if (str === false) {
|
|
str = c;
|
|
content += c;
|
|
continue;
|
|
} else if ((c == "'" && str == "'") || (c == '"' && str == '"')) {
|
|
str = false;
|
|
content += c;
|
|
continue;
|
|
}
|
|
} else if (str === false && (c == "[" || c == "{")) {
|
|
++count;
|
|
} else if (str === false && (c == "]" || c == "}")) {
|
|
--count;
|
|
} else if (str === false && count == 0 && c == ",") {
|
|
res.push(content);
|
|
content = "";
|
|
continue;
|
|
}
|
|
|
|
content += c;
|
|
}
|
|
|
|
if (content.length > 0) res.push(content);
|
|
|
|
var newRes = {};
|
|
for (var j = 0, lenJ = res.length; j < lenJ; ++j) {
|
|
if ((m = res[j].match(regex["key_value"]))) {
|
|
newRes[m[1]] = processValue(m[2]);
|
|
}
|
|
}
|
|
|
|
return newRes;
|
|
} else return val;
|
|
}
|
|
|
|
function processFoldedBlock(block) {
|
|
var lines = block.lines;
|
|
var children = block.children;
|
|
var str = lines.join(" ");
|
|
var chunks = [str];
|
|
for (var i = 0, len = children.length; i < len; ++i) {
|
|
chunks.push(processFoldedBlock(children[i]));
|
|
}
|
|
return chunks.join("\n");
|
|
}
|
|
|
|
function processLiteralBlock(block) {
|
|
var lines = block.lines;
|
|
var children = block.children;
|
|
var str = lines.join("\n");
|
|
for (var i = 0, len = children.length; i < len; ++i) {
|
|
str += processLiteralBlock(children[i]);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
function processBlock(blocks) {
|
|
var m = null;
|
|
var res = {};
|
|
var lines = null;
|
|
var children = null;
|
|
var currentObj = null;
|
|
|
|
var level = -1;
|
|
|
|
var processedBlocks = [];
|
|
|
|
var isMap = true;
|
|
|
|
for (var j = 0, lenJ = blocks.length; j < lenJ; ++j) {
|
|
if (level != -1 && level != blocks[j].level) continue;
|
|
|
|
processedBlocks.push(j);
|
|
|
|
level = blocks[j].level;
|
|
lines = blocks[j].lines;
|
|
children = blocks[j].children;
|
|
currentObj = null;
|
|
|
|
for (var i = 0, len = lines.length; i < len; ++i) {
|
|
var line = lines[i];
|
|
|
|
if ((m = line.match(regex["key"]))) {
|
|
var key = m[1];
|
|
|
|
if (key[0] == "-") {
|
|
key = key.replace(regex["item"], "");
|
|
if (isMap) {
|
|
isMap = false;
|
|
if (typeof res.length === "undefined") {
|
|
res = [];
|
|
}
|
|
}
|
|
if (currentObj != null) res.push(currentObj);
|
|
currentObj = {};
|
|
isMap = true;
|
|
}
|
|
|
|
if (typeof m[2] != "undefined") {
|
|
var value = m[2].replace(regex["trim"], "");
|
|
if (value[0] == "&") {
|
|
var nb = processBlock(children);
|
|
if (currentObj != null) currentObj[key] = nb;
|
|
else res[key] = nb;
|
|
reference_blocks[value.substr(1)] = nb;
|
|
} else if (value[0] == "|") {
|
|
if (currentObj != null)
|
|
currentObj[key] = processLiteralBlock(children.shift());
|
|
else res[key] = processLiteralBlock(children.shift());
|
|
} else if (value[0] == "*") {
|
|
var v = value.substr(1);
|
|
var no = {};
|
|
|
|
if (typeof reference_blocks[v] == "undefined") {
|
|
errors.push("Reference '" + v + "' not found!");
|
|
} else {
|
|
for (var k in reference_blocks[v]) {
|
|
no[k] = reference_blocks[v][k];
|
|
}
|
|
|
|
if (currentObj != null) currentObj[key] = no;
|
|
else res[key] = no;
|
|
}
|
|
} else if (value[0] == ">") {
|
|
if (currentObj != null)
|
|
currentObj[key] = processFoldedBlock(children.shift());
|
|
else res[key] = processFoldedBlock(children.shift());
|
|
} else {
|
|
if (currentObj != null) currentObj[key] = processValue(value);
|
|
else res[key] = processValue(value);
|
|
}
|
|
} else {
|
|
if (currentObj != null) currentObj[key] = processBlock(children);
|
|
else res[key] = processBlock(children);
|
|
}
|
|
} else if (line.match(/^-\s*$/)) {
|
|
if (isMap) {
|
|
isMap = false;
|
|
if (typeof res.length === "undefined") {
|
|
res = [];
|
|
}
|
|
}
|
|
if (currentObj != null) res.push(currentObj);
|
|
currentObj = {};
|
|
isMap = true;
|
|
} else if ((m = line.match(/^-\s*(.*)/))) {
|
|
if (currentObj != null) currentObj.push(processValue(m[1]));
|
|
else {
|
|
if (isMap) {
|
|
isMap = false;
|
|
if (typeof res.length === "undefined") {
|
|
res = [];
|
|
}
|
|
}
|
|
res.push(processValue(m[1]));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentObj != null) {
|
|
if (isMap) {
|
|
isMap = false;
|
|
if (typeof res.length === "undefined") {
|
|
res = [];
|
|
}
|
|
}
|
|
res.push(currentObj);
|
|
}
|
|
}
|
|
|
|
for (var j = processedBlocks.length - 1; j >= 0; --j) {
|
|
blocks.splice.call(blocks, processedBlocks[j], 1);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
function semanticAnalysis(blocks) {
|
|
var res = processBlock(blocks.children);
|
|
return res;
|
|
}
|
|
|
|
function preProcess(src) {
|
|
var m;
|
|
var lines = src.split("\n");
|
|
|
|
var r = regex["comment"];
|
|
|
|
for (var i in lines) {
|
|
if ((m = typeof lines[i] === "string" && lines[i].match(r))) {
|
|
/* var cmt = "";
|
|
if(typeof m[3] != "undefined")
|
|
lines[i] = m[1];
|
|
else if(typeof m[3] != "undefined")
|
|
lines[i] = m[3];
|
|
else
|
|
lines[i] = "";
|
|
*/
|
|
if (typeof m[3] !== "undefined") {
|
|
lines[i] = m[0].substr(0, m[0].length - m[3].length);
|
|
}
|
|
}
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function eval(str) {
|
|
errors = [];
|
|
reference_blocks = [];
|
|
processing_time = new Date().getTime();
|
|
var pre = preProcess(str);
|
|
var doc = parser(pre);
|
|
var res = semanticAnalysis(doc);
|
|
processing_time = new Date().getTime() - processing_time;
|
|
|
|
return res;
|
|
}
|
|
|
|
return {
|
|
/**
|
|
* Parse a YAML file from a string.
|
|
* @param {String} str String with the YAML file contents.
|
|
* @function
|
|
*/
|
|
eval: eval,
|
|
|
|
/**
|
|
* Get errors found when parsing the last file.
|
|
* @function
|
|
* @returns Errors found when parsing the last file.
|
|
*/
|
|
getErrors: function () {
|
|
return errors;
|
|
},
|
|
|
|
/**
|
|
* Get the time it took to parse the last file.
|
|
* @function
|
|
* @returns Time in milliseconds.
|
|
*/
|
|
getProcessingTime: function () {
|
|
return processing_time;
|
|
},
|
|
};
|
|
})(); |