From d703a057b7fa7a635aa1e294c62d9e31b0c2ef9f Mon Sep 17 00:00:00 2001
From: Peng-YM <1048217874pengym@gmail.com>
Date: Mon, 17 Aug 2020 22:00:42 +0800
Subject: [PATCH] Add parser
---
.idea/.gitignore | 5 +
.idea/MagicStore.iml | 12 +
.idea/dictionaries/pengym.xml | 7 +
.idea/misc.xml | 6 +
.idea/modules.xml | 8 +
node_modules/request | 1 +
parser.js | 1614 +++++++++++++++++++++++++++++++++
root.json | 1 +
8 files changed, 1654 insertions(+)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/MagicStore.iml
create mode 100644 .idea/dictionaries/pengym.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/modules.xml
create mode 120000 node_modules/request
create mode 100644 parser.js
create mode 100644 root.json
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/MagicStore.iml b/.idea/MagicStore.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/MagicStore.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/pengym.xml b/.idea/dictionaries/pengym.xml
new file mode 100644
index 0000000..f54b023
--- /dev/null
+++ b/.idea/dictionaries/pengym.xml
@@ -0,0 +1,7 @@
+
+
+
+ obfs
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..28a804d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..8fe30fa
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/node_modules/request b/node_modules/request
new file mode 120000
index 0000000..1273f8c
--- /dev/null
+++ b/node_modules/request
@@ -0,0 +1 @@
+../../../../.nvm/versions/node/v14.4.0/lib/node_modules/request
\ No newline at end of file
diff --git a/parser.js b/parser.js
new file mode 100644
index 0000000..1225e5d
--- /dev/null
+++ b/parser.js
@@ -0,0 +1,1614 @@
+const $ = API("my-store");
+
+// SOME CONSTANTS
+const DEFAULT_SUPPORTED_PLATFORMS = {
+ QX: true,
+ Loon: true,
+ Surge: true,
+ Clash: true
+}
+
+const $parser = ProxyParser("QX");
+
+;(async () => {
+ // Test QX format
+ // const URL = "https://raw.githubusercontent.com/crossutility/Quantumult-X/master/server-complete.txt";
+
+ // Test SS URI
+ // const URL = "https://gist.githubusercontent.com/Peng-YM/ace5e187b28dc90350df70a4d19d415a/raw/ad3b02a29eef46912aa45aeab4eddd4b90eb9cdb/server_complete.txt";
+
+ // Test Based64 encoded format
+ const URL = "http://127.0.0.1:8080/nex.list";
+
+ // Test Loon format
+ // const URL = "https://skapi.cool/sub?target=loon&url=https%3A%2F%2Fraw.githubusercontent.com%2Fcrossutility%2FQuantumult-X%2Fmaster%2Fserver-complete.txt&insert=false&config=https%3A%2F%2Fraw.githubusercontent.com%2FACL4SSR%2FACL4SSR%2Fmaster%2FClash%2Fconfig%2FACL4SSR_Online.ini&emoji=true&list=true&udp=false&tfo=false&scv=false&fdn=false&sort=false";
+
+ // Test Surge format
+ // const URL = "https://skapi.cool/sub?target=surge&ver=4&url=https%3A%2F%2Fraw.githubusercontent.com%2Fcrossutility%2FQuantumult-X%2Fmaster%2Fserver-complete.txt&insert=false&config=https%3A%2F%2Fraw.githubusercontent.com%2FACL4SSR%2FACL4SSR%2Fmaster%2FClash%2Fconfig%2FACL4SSR_Online.ini&emoji=true&list=true&udp=false&tfo=false&scv=false&fdn=false&sort=false";
+
+ const raw = await $.http.get(URL).then(resp => resp.body);
+
+ let proxies = $parser.parse(raw);
+
+ // filters
+ const $filter = ProxyFilter();
+ $filter.addFilters(
+ KeywordFilter(["Hong Kong", "Singapore", "USA", "Taiwan", "Japan"]),
+ DiscardKeywordFilter("[Premium]")
+ );
+ proxies = $filter.process(proxies);
+
+ // operators
+ const $operator = ProxyOperator();
+ $operator.addOperators(
+ SetPropertyOperator('tfo', true),
+ FlagOperator(1),
+ SortOperator('asc'),
+ KeywordRenameOperator([
+ {old: "Hong Kong", now: "HK"},
+ {old: "Japan", now: "JP"},
+ {old: "Taiwan", now: "TW"},
+ {old: "Singapore", now: "SGP"}
+ ])
+ );
+ proxies = $operator.process(proxies);
+
+
+ console.log($parser.produce(proxies));
+})();
+
+function ProxyParser(targetPlatform) {
+ // parser collections
+ const parsers = [];
+ const producers = [];
+
+ function addParsers(...args) {
+ args.forEach(a => parsers.push(a()));
+ }
+
+ function addProducers(...args) {
+ args.forEach(a => producers.push(a()))
+ }
+
+ function parse(raw) {
+ raw = preprocessing(raw);
+ const lines = raw.split("\n");
+ const result = [];
+ // convert to json format
+ for (let line of lines) {
+ line = line.trim();
+ if (line.length === 0) continue; // skip empty line
+ if (line.startsWith("#")) continue; // skip comments
+ let matched = false;
+ for (const p of parsers) {
+ const {patternTest, func} = p;
+
+ // some lines with weird format may produce errors!
+ let patternMatched;
+ try {
+ patternMatched = patternTest(line);
+ } catch (err) {
+ patternMatched = false;
+ }
+
+ if (patternMatched) {
+ matched = true;
+ // run parser safely.
+ try {
+ const proxy = func(line);
+ if (!proxy) {
+ // failed to parse this line
+ console.log(`ERROR: parser return nothing for \n${line}\n`);
+ break;
+ }
+ // skip unsupported proxies
+ // if proxy.supported is undefined, assume that all platforms are supported.
+ if (typeof proxy.supported === 'undefined' || proxy.supported[targetPlatform]) {
+ delete proxy.supported;
+ result.push(proxy);
+ break;
+ }
+ } catch (err) {
+ console.log(`ERROR: Failed to parse line: \n ${line}\n Reason: ${err}`);
+ }
+ }
+ }
+ if (!matched) {
+ console.log(`ERROR: Failed to find a rule to parse line: \n${line}\n`);
+ }
+ }
+ if (result.length === 0) {
+ throw new Error(`ERROR: Input does not contains any valid node for platform ${targetPlatform}`)
+ }
+ return result;
+ }
+
+ function produce(proxies) {
+ for (const p of producers) {
+ if (p.targetPlatform === targetPlatform) {
+ return proxies.map(proxy => {
+ try {
+ return p.output(proxy)
+ } catch (err) {
+ console.log(`ERROR: cannot produce proxy: ${JSON.stringify(proxy)}\nReason: ${err}`);
+ return "";
+ }
+ }).join("\n");
+ }
+ }
+ throw new Error(`Cannot find any producer for target platform: ${targetPlatform}`);
+ }
+
+ // preprocess raw input
+ function preprocessing(raw) {
+ let output;
+ if (raw.indexOf("DOCTYPE html") !== -1) {
+ // HTML format, maybe a wrong URL!
+ throw new Error("Invalid format HTML!");
+ }
+ // check if content is based64 encoded
+ const Base64 = new Base64Code();
+ const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v"];
+ if (keys.some(k => raw.indexOf(k) !== -1)) {
+ output = Base64.safeDecode(raw);
+ } else {
+ output = raw;
+ }
+ output = output.split("\n");
+ for (let i = 0; i < output.length; i++) {
+ output[i] = output[i].trim(); // trim lines
+ }
+ return output.join("\n");
+ }
+
+ /********************* PARSERS *******************************/
+
+ addParsers(
+ // URI format parsers
+ URI_SS, URI_SSR, URI_VMess, URI_Trojan,
+ // Quantumult X platform
+ QX_SS, QX_SSR, QX_VMess, QX_Trojan, QX_Http,
+ // Loon platform
+ Loon_SS, Loon_SSR, Loon_VMess, Loon_Trojan, Loon_Http,
+ // Surge platform
+ Surge_SS, Surge_VMess, Surge_Trojan, Surge_Http
+ );
+
+ /********************* PRODUCERS *******************************/
+ addProducers(
+ QX_Producer, Loon_Producer, Surge_Producer, Clash_Producer
+ );
+
+ return {
+ parse, produce
+ };
+}
+
+function ProxyFilter() {
+ const filters = [];
+
+ function addFilters(...args) {
+ args.forEach(a => filters.push(a));
+ }
+
+ // select proxies
+ function process(proxies) {
+ let selected = FULL(proxies.length, true);
+ for (const filter of filters) {
+ try {
+ selected = AND(selected, filter.func(proxies));
+ } catch (err) {
+ console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
+ }
+ }
+ return proxies.filter((_, i) => selected[i])
+ }
+
+ return {
+ process, addFilters
+ }
+}
+
+function ProxyOperator() {
+ const operators = [];
+
+ function addOperators(...args) {
+ args.forEach(a => operators.push(a));
+ }
+
+ // run all operators
+ function process(proxies) {
+ let output = clone(proxies);
+ for (const op of operators) {
+ try {
+ const output_ = op.func(output);
+ if (output_) output = output_;
+ } catch (err) {
+ // print log and skip this operator
+ console.log(`ERROR: cannot apply operator ${op.name}! Reason: ${err}`);
+ }
+ }
+ return output;
+ }
+
+ return {addOperators, process}
+}
+
+function RuleParser(targetPlatform) {
+ // TODO
+}
+
+function RuleFilter() {
+ // TODO
+}
+
+function RuleOperator() {
+ // TODO
+}
+
+/**************************** URI Format ***************************************/
+// 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 patternTest = (line) => {
+ return /^ss:\/\//.test(line);
+ }
+ const Base64 = new Base64Code();
+ const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
+ const func = (line) => {
+ // 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
+
+ proxy.server = content.match(/@([^\/]*)\//)[1].split(":")[0];
+ proxy.port = content.match(/@([^\/]*)\//)[1].split(":")[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(";");
+ 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 'simple-obfs':
+ proxy.plugin = 'obfs'
+ proxy['plugin-opts'] = {
+ mode: params.obfs,
+ host: params['obfs-host']
+ }
+ break
+ case 'v2ray-plugin':
+ proxy.supported = {
+ ...DEFAULT_SUPPORTED_PLATFORMS,
+ Loon: false,
+ Surge: false
+ }
+ proxy.obfs = 'v2ray-plugin'
+ proxy['plugin-opts'] = {
+ mode: "websocket",
+ host: params['obfs-host'],
+ path: params.path || ""
+ }
+ break
+ default:
+ throw new Error(`Unsupported plugin option: ${params.plugin}`)
+ }
+ }
+ return proxy;
+ }
+ return {patternTest, func};
+}
+
+// Parse URI SSR format, such as ssr://xxx
+function URI_SSR() {
+ const patternTest = (line) => {
+ return /^ssr:\/\//.test(line);
+ }
+ const Base64 = new Base64Code();
+ const supported = {
+ ...DEFAULT_SUPPORTED_PLATFORMS,
+ Surge: false
+ }
+
+ const func = (line) => {
+ line = Base64.safeDecode(line.split("ssr://")[1]);
+ let params = line.split("/?")[0].split(":");
+ let proxy = {
+ type: "ssr",
+ server: params[0],
+ port: params[1],
+ protocol: params[2],
+ cipher: params[3],
+ obfs: params[4],
+ password: Base64.safeDecode(params[5]),
+ supported
+ }
+ // get other params
+ params = {};
+ line = line.split("/?")[1].split("&");
+ if (line.length > 1) {
+ for (const item of line) {
+ const [key, val] = item.split("=");
+ params[key] = val;
+ }
+ }
+ proxy = {
+ ...proxy,
+ name: Base64.safeDecode(params.remarks),
+ "protocol-param": Base64.safeDecode(params.protoparam).replace(/\s/g, ""),
+ "obfs-param": Base64.safeDecode(params.obfsparam).replace(/\s/g, "")
+ }
+ return proxy;
+ }
+
+ return {patternTest, func};
+}
+
+// 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)
+function URI_VMess() {
+ const patternTest = (line) => {
+ return /^vmess:\/\//.test(line);
+ }
+ const Base64 = new Base64Code();
+ const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
+ const func = (line) => {
+ line = line.split("vmess://")[1];
+ const params = JSON.parse(Base64.safeDecode(line));
+ 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: JSON.parse(params.tls || "false"),
+ supported
+ }
+ // handle obfs
+ if (params.net === 'ws') {
+ proxy.network = 'ws';
+ proxy['ws-path'] = params.path;
+ proxy['ws-headers'] = {
+ Host: params.host || params.add
+ }
+ }
+ return proxy
+ }
+ return {patternTest, func};
+}
+
+// Trojan URI format
+function URI_Trojan() {
+ const patternTest = (line) => {
+ return /^trojan:\/\//.test(line);
+ }
+ const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
+ const func = (line) => {
+ // trojan forces to use 443 port
+ if (line.indexOf(":443") === -1) {
+ throw new Error("Trojan port should always be 443!");
+ }
+ line = line.split("trojan://")[1];
+ const server = line.split("@")[1].split(":443")[0];
+
+ return {
+ name: `[Trojan] ${server}`, // trojan uri has no server tag!
+ type: "trojan",
+ server,
+ port: 443,
+ password: line.split("@")[0],
+ supported
+ }
+ }
+ return {patternTest, func};
+}
+
+/**************************** Quantumult X ***************************************/
+function QX_SS() {
+ const patternTest = (line) => {
+ return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") === -1;
+ };
+ const func = (line) => {
+ 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: clone(DEFAULT_SUPPORTED_PLATFORMS)
+ };
+ // 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'
+ }
+ 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 {patternTest, func};
+}
+
+function QX_SSR() {
+ const patternTest = (line) => {
+ return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") !== -1;
+ };
+ const supported = {
+ ...DEFAULT_SUPPORTED_PLATFORMS,
+ Surge: false
+ }
+ const func = (line) => {
+ 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 {patternTest, func};
+}
+
+function QX_VMess() {
+ const patternTest = (line) => {
+ return /^vmess\s*=/.test(line.split(",")[0].trim());
+ };
+ const func = (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.scert = !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 {patternTest, func};
+}
+
+function QX_Trojan() {
+ const patternTest = (line) => {
+ return /^trojan\s*=/.test(line.split(",")[0].trim());
+ };
+ const func = (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.scert = !JSON.parse(params['tls-verification'] || 'true');
+ return proxy;
+ }
+ return {patternTest, func}
+}
+
+function QX_Http() {
+ const patternTest = (line) => {
+ return /^http\s*=/.test(line.split(",")[0].trim());
+ };
+ const func = (line) => {
+ const params = getQXParams(line);
+ const proxy = {
+ type: "http",
+ name: params.tag,
+ server: params.server,
+ port: params.port,
+ username: params.username,
+ password: params.password,
+ tls: JSON.parse(params['over-tls'] || "false"),
+ udp: JSON.parse(params["udp-relay"] || "false"),
+ tfo: JSON.parse(params["fast-open"] || "false"),
+ }
+ if (proxy.tls) {
+ proxy.sni = params['tls-host'] || proxy.server;
+ proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
+ }
+ return proxy;
+ }
+
+ return {patternTest, func};
+}
+
+function getQXParams(line) {
+ const groups = line.split(",");
+ const params = {};
+ const protocols = ["shadowsocks", "vmess", "http", "trojan"];
+ groups.forEach((g) => {
+ const [key, value] = g.split("=");
+ 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;
+}
+
+/**************************** Loon ***************************************/
+function Loon_SS() {
+ const patternTest = (line) => {
+ return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocks';
+ }
+ const func = (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: proxy.obfs,
+ host: params[6]
+ }
+ }
+ return proxy;
+ }
+ return {patternTest, func};
+}
+
+function Loon_SSR() {
+ const patternTest = (line) => {
+ return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocksr';
+ }
+ const func = (line) => {
+ const params = line.split("=")[1].split(",");
+ const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
+ 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 {patternTest, func};
+}
+
+function Loon_VMess() {
+ const patternTest = (line) => {
+ // distinguish between surge vmess
+ return /^.*=\s*vmess/i.test(line.split(",")[0]) && line.indexOf("username") === -1;
+ }
+ const func = (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.scert = 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.scert = JSON.parse(params['skip-cert-verify'] || 'false')
+ }
+ return proxy;
+ }
+ return {patternTest, func};
+}
+
+function Loon_Trojan() {
+ const patternTest = (line) => {
+ return /^.*=\s*trojan/i.test(line.split(",")[0]) && line.indexOf("password") === -1;
+ }
+
+ const func = (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
+ scert: 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(`ERROR: unknown option ${key} for line: \n${line}`);
+ }
+ return proxy;
+ }
+
+ return {patternTest, func}
+}
+
+function Loon_Http() {
+ const patternTest = (line) => {
+ return /^.*=\s*http/i.test(line.split(",")[0])
+ && line.split(",").length === 5
+ && line.indexOf("username") === -1
+ && line.indexOf("password") === -1
+ }
+
+ const func = (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
+ username: (params[3] || "").replace(/"/g, ""),
+ password: (params[4] || "").replace(/"/g, "")
+ }
+ if (proxy.tls) {
+ proxy.sni = params['tls-name'] || proxy.server;
+ proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
+ }
+
+ return proxy;
+ }
+ return {patternTest, func}
+}
+
+/**************************** Surge ***************************************/
+function Surge_SS() {
+ const patternTest = (line) => {
+ return /^.*=\s*ss/.test(line.split(",")[0]);
+ }
+ const func = (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 {patternTest, func}
+}
+
+function Surge_VMess() {
+ const patternTest = (line) => {
+ return /^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1;
+ }
+ const func = (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) {
+ proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
+ proxy.sni = params['sni'] || params.server;
+ }
+ // use websocket
+ if (JSON.parse(params.ws || "false")) {
+ proxy.network = 'ws';
+ proxy['ws-path'] = params['ws-path'];
+ proxy['ws-headers'] = {
+ Host: params.sni
+ }
+ }
+ return proxy;
+ }
+ return {patternTest, func};
+}
+
+function Surge_Trojan() {
+ const patternTest = (line) => {
+ return /^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1;
+ }
+ const func = (line) => {
+ const params = getSurgeParams(line);
+ return {
+ 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"),
+ scert: JSON.parse(params['skip-cert-verify'] || "false"),
+ }
+ }
+
+ return {patternTest, func};
+}
+
+function Surge_Http() {
+ const patternTest = (line) => {
+ return /^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().patternTest(line)
+ }
+ const func = (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) {
+ proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
+ proxy.sni = params.sni || params.server;
+ }
+ if (params.username !== 'none') proxy.username = params.username;
+ if (params.password !== 'none') proxy.password = params.password;
+ return proxy;
+ }
+ return {patternTest, func}
+}
+
+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;
+}
+
+/**************************** Output Functions ***************************************/
+function QX_Producer() {
+ const targetPlatform = "QX";
+ const output = (proxy) => {
+ let obfs_opts;
+ let tls_opts;
+ switch (proxy.type) {
+ case 'ss':
+ obfs_opts = "";
+ if (proxy.plugin === 'obfs') {
+ obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`;
+ }
+ if (proxy.plugin === 'v2ray-plugin') {
+ const {tls, host, path} = proxy['plugin-opts'];
+ obfs_opts = `,obfs=${tls ? 'wss' : 'ws'},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},ssr-protocol-param=${proxy['protocol-param']},obfs=${proxy.obfs},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,obfs-host=${proxy.sni}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""},tls-verification=${proxy.scert ? "false" : "true"}`;
+ } else {
+ // ws
+ obfs_opts = `,obfs=ws,obfs-host=${proxy['ws-headers'].Host}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""}`;
+ }
+ } else {
+ // tcp
+ if (proxy.tls) {
+ obfs_opts = `,obfs=over-tls,obfs-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}`;
+ }
+ }
+ return `vmess=${proxy.server}:${proxy.port},method=${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},tls-host=${proxy.sni},tls-verification=${proxy.scert ? "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.scert ? "false" : "true"},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 {targetPlatform, output};
+}
+
+function Loon_Producer() {
+ const targetPlatform = "Loon";
+ const output = (proxy) => {
+ let obfs_opts, tls_opts;
+ switch (proxy.type) {
+ case "ss":
+ obfs_opts = ",,";
+ if (proxy.plugin === 'obfs') {
+ const {mode, host} = proxy['plugin-opts'];
+ obfs_opts = `,${mode},${host}`
+ }
+ return `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password}${obfs_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;
+ obfs_opts = `,transport:ws,host:${host},path:${proxy['ws-path']}`;
+ } else {
+ obfs_opts = `,transport:tcp`;
+ }
+ if (proxy.tls) {
+ obfs_opts += `,tls-name=${proxy.sni},skip-cert-verify:${proxy.scert}`;
+ }
+ return `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},over-tls:${proxy.tls}${obfs_opts}`;
+ case "trojan":
+ return `${proxy.name}=trojan,${proxy.server},${proxy.port},${proxy.password},tls-name:${proxy.sni},skip-cert-verify:${proxy.scert}`;
+ 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 = `,skip-cert-verify:${proxy.scert},tls-name:${proxy.sni}`;
+ return base + tls_opts;
+ } else return base;
+ }
+ throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
+ }
+ return {targetPlatform, output}
+}
+
+function Surge_Producer() {
+ const targetPlatform = "Surge";
+ const output = (proxy) => {
+ let obfs_opts, tls_opts;
+ switch (proxy.type) {
+ case 'ss':
+ obfs_opts = "";
+ if (proxy.plugin === "obfs") {
+ obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].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},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 += `,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
+ }
+ return config;
+ case 'trojan':
+ return `${proxy.name}=trojan,${proxy.server},${proxy.port},password=${proxy.password},sni=${proxy.sni},tfo=${proxy.tfo || 'false'}`;
+ case 'http':
+ tls_opts = ",tls=false";
+ if (proxy.tls) {
+ tls_opts = `,tls=true,skip-cert-verify=${proxy.scert},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 {targetPlatform, output}
+}
+
+function Clash_Producer() {
+ const targetPlatform = "Clash";
+ const output = proxy => JSON.stringify(proxy)
+}
+
+/**************************** Operators ***************************************/
+// force to set some properties (e.g., scert, udp, tfo, etc.)
+function SetPropertyOperator(key, val) {
+ return {
+ name: "Set",
+ func: proxies => {
+ return proxies.map(p => {
+ p[key] = val;
+ return p;
+ })
+ }
+ }
+}
+
+// add or remove flag for proxies
+function FlagOperator(type) {
+ return {
+ name: "Flag",
+ func: proxies => {
+ return proxies.map(proxy => {
+ switch (type) {
+ case 0:
+ // no flag
+ proxy.name = removeFlag(proxy.name);
+ break
+ case 1:
+ // 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, "🇨🇳");
+ break;
+ default:
+ throw new Error("Unknown flag type: " + type);
+ }
+ return proxy;
+ })
+ }
+ }
+}
+
+// sort proxies according to their names
+function SortOperator(order = 'asc') {
+ return {
+ name: "Sort",
+ func: proxies => {
+ switch (order) {
+ case "asc":
+ case 'desc':
+ return proxies.sort((a, b) => {
+ let res = (a > b) ? -1 : 1;
+ res *= order === 'desc' ? -1 : 1;
+ return res
+ })
+ case 'random':
+ return shuffle(proxies);
+ default:
+ throw new Error("Unknown sort option: " + order);
+ }
+ }
+ }
+}
+
+// rename by keywords
+// keywords: [{old: "old", now: "now"}]
+function KeywordRenameOperator(keywords) {
+ return {
+ name: "Keyword Rename",
+ func: proxies => {
+ return proxies.map(proxy => {
+ for (const {old, now} of keywords) {
+ proxy.name = proxy.name.replace(old, now);
+ }
+ return proxy;
+ })
+ }
+ }
+}
+
+// rename by regex
+// keywords: [{expr: "string format regex", now: "now"}]
+function RegexRenameOperator(regex) {
+ if (!(regex instanceof Array)) regex = [regex];
+ return {
+ name: "Regex Rename",
+ func: proxies => {
+ return proxies.map(proxy => {
+ for (const {expr, now} of regex) {
+ proxy.name = proxy.name.replace(new RegExp(expr, "g"), now);
+ }
+ return proxy;
+ })
+ }
+ }
+}
+
+// delete keywords operator
+// keywords: ['a', 'b', 'c']
+function KeywordDeleteOperator(keywords) {
+ if (!(keywords instanceof Array)) keywords = [keywords];
+ const keywords_ = keywords.map(k => {
+ return {
+ old: k,
+ now: ""
+ }
+ })
+ return {
+ name: "Keyword Delete",
+ func: KeywordRenameOperator(keywords_).func
+ }
+}
+
+// delete regex operator
+// regex: ['a', 'b', 'c']
+function RegexDeleteOperator(regex) {
+ if (!(regex instanceof Array)) regex = [regex];
+ const regex_ = regex.map(r => {
+ return {
+ expr: r,
+ now: ""
+ }
+ });
+ return {
+ name: "Regex Delete",
+ func: RegexRenameOperator(regex_).func
+ }
+}
+
+// use base64 encoded script to rename
+/** Example script
+ function rename(proxies) {
+ // do something
+ return proxies;
+ }
+
+ WARNING:
+ 1. This function name should be `rename`!
+ 2. Always declare variable before using it!
+ */
+function ScriptRenameOperator(script, encoded = true) {
+ if (encoded) {
+ const Base64 = new Base64Code();
+ script = Base64.safeDecode(script);
+ }
+
+ return {
+ name: "Script Rename",
+ func: (proxies) => {
+ ;(function () {
+ eval(script);
+ return rename(proxies);
+ })();
+ }
+ }
+}
+
+/**************************** Filters ***************************************/
+function KeywordFilter(keywords) {
+ if (!(keywords instanceof Array)) keywords = [keywords];
+ return {
+ name: "Keyword Filter",
+ func: (proxies) => {
+ return proxies.map(proxy => keywords.some(k => proxy.name.indexOf(k) !== -1));
+ }
+ }
+}
+
+function DiscardKeywordFilter(keywords) {
+ if (!(keywords instanceof Array)) keywords = [keywords];
+ return {
+ name: "Discard Keyword Filter",
+ func: proxies => {
+ const filter = KeywordFilter(keywords).func;
+ return NOT(filter(proxies));
+ }
+ }
+}
+
+function RegexFilter(regex) {
+ if (!(regex instanceof Array)) regex = [regex];
+ return {
+ name: "Regex Filter",
+ func: (proxies) => {
+ return proxies.map(proxy => regex.some(r => r.test(proxy.name)));
+ }
+ }
+}
+
+function DiscardRegexFilter(regex) {
+ if (!(regex instanceof Array)) regex = [regex];
+ return {
+ name: "Discard Regex Filter",
+ func: proxies => {
+ const filter = RegexFilter(regex).func;
+ return NOT(filter(proxies));
+ }
+ }
+}
+
+function TypeFilter(types) {
+ if (!(types instanceof Array)) types = [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 filter(proxies) {
+ const selected = FULL(proxies.length, true);
+ // do something
+ return selected;
+ }
+ */
+function ScriptFilter(script, encoded = true) {
+ if (encoded) {
+ const Base64 = new Base64Code();
+ script = Base64.safeDecode(script);
+ }
+ return {
+ name: "Script Filter",
+ func: (proxies) => {
+ !(function () {
+ eval(script);
+ return filter(proxies);
+ })();
+ }
+ }
+}
+
+/******************************** 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 = {
+ "🏳️🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"],
+ "🇦🇨": ["AC"],
+ "🇦🇹": ["奥地利", "维也纳"],
+ "🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"],
+ "🇧🇪": ["BE", "比利时"],
+ "🇧🇬": ["保加利亚", "Bulgaria"],
+ "🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"],
+ "🇨🇦": ["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"]
+ };
+ 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();
+}
+
+// clone an object
+function clone(obj) {
+ return JSON.parse(JSON.stringify(obj))
+}
+
+// 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;
+}
+
+// some logical functions for proxy filters
+function AND(...args) {
+ return args.reduce((a, b) => a.map((c, i) => b[i] && c));
+}
+
+function OR(...args) {
+ return args.reduce((a, b) => a.map((c, i) => b[i] || c))
+}
+
+function NOT(array) {
+ return array.map(c => !c);
+}
+
+function FULL(length, bool) {
+ return [...Array(length).keys()].map(() => bool);
+}
+
+/*********************************** OpenAPI *************************************/
+// OpenAPI
+// prettier-ignore
+function ENV() {
+ const e = "undefined" != typeof $task, t = "undefined" != typeof $loon,
+ s = "undefined" != typeof $httpClient && !this.isLoon,
+ o = "function" == typeof require && "undefined" != typeof $jsbox;
+ return {isQX: e, isLoon: t, isSurge: s, isNode: "function" == typeof require && !o, isJSBox: o}
+}
+
+function HTTP(e, t = {}) {
+ const {isQX: s, isLoon: o, isSurge: n} = ENV();
+ const i = {};
+ return ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"].forEach(r => i[r.toLowerCase()] = (i => (function (i, r) {
+ (r = "string" == typeof r ? {url: r} : r).url = e ? e + r.url : r.url;
+ const u = (r = {...t, ...r}).timeout, h = {
+ onRequest: () => {
+ }, onResponse: e => e, onTimeout: () => {
+ }, ...r.events
+ };
+ let c, l;
+ h.onRequest(i, r), c = s ? $task.fetch({method: i, ...r}) : new Promise((e, t) => {
+ (n || o ? $httpClient : require("request"))[i.toLowerCase()](r, (s, o, n) => {
+ s ? t(s) : e({statusCode: o.status || o.statusCode, headers: o.headers, body: n})
+ })
+ });
+ const a = u ? new Promise((e, t) => {
+ l = setTimeout(() => (h.onTimeout(), t(`${i} URL: ${r.url} exceeds the timeout ${u} ms`)), u)
+ }) : null;
+ return (a ? Promise.race([a, c]).then(e => (clearTimeout(l), e)) : c).then(e => h.onResponse(e))
+ })(r, i))), i
+}
+
+function API(e = "untitled", t = !1) {
+ const {isQX: s, isLoon: o, isSurge: n, isNode: i, isJSBox: r} = ENV();
+ return new class {
+ constructor(e, t) {
+ this.name = e, this.debug = t, this.http = HTTP(), this.env = ENV(), this.node = (() => {
+ if (i) {
+ return {fs: require("fs")}
+ }
+ return null
+ })(), this.initCache();
+ Promise.prototype.delay = function (e) {
+ return this.then(function (t) {
+ return ((e, t) => new Promise(function (s) {
+ setTimeout(s.bind(null, t), e)
+ }))(e, t)
+ })
+ }
+ }
+
+ initCache() {
+ if (s && (this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}")), (o || n) && (this.cache = JSON.parse($persistentStore.read(this.name) || "{}")), i) {
+ let e = "root.json";
+ this.node.fs.existsSync(e) || this.node.fs.writeFileSync(e, JSON.stringify({}), {flag: "wx"}, e => console.log(e)), this.root = {}, e = `${this.name}.json`, this.node.fs.existsSync(e) ? this.cache = JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)) : (this.node.fs.writeFileSync(e, JSON.stringify({}), {flag: "wx"}, e => console.log(e)), this.cache = {})
+ }
+ }
+
+ persistCache() {
+ const e = JSON.stringify(this.cache);
+ s && $prefs.setValueForKey(e, this.name), (o || n) && $persistentStore.write(e, this.name), i && (this.node.fs.writeFileSync(`${this.name}.json`, e, {flag: "w"}, e => console.log(e)), this.node.fs.writeFileSync("root.json", JSON.stringify(this.root), {flag: "w"}, e => console.log(e)))
+ }
+
+ write(e, t) {
+ this.log(`SET ${t}`), -1 !== t.indexOf("#") ? (t = t.substr(1), n & o && $persistentStore.write(e, t), s && $prefs.setValueForKey(e, t), i && (this.root[t] = e)) : this.cache[t] = e, this.persistCache()
+ }
+
+ read(e) {
+ return this.log(`READ ${e}`), -1 === e.indexOf("#") ? this.cache[e] : (e = e.substr(1), n & o ? $persistentStore.read(e) : s ? $prefs.valueForKey(e) : i ? this.root[e] : void 0)
+ }
+
+ delete(e) {
+ this.log(`DELETE ${e}`), -1 !== e.indexOf("#") ? (e = e.substr(1), n & o && $persistentStore.write(null, e), s && $prefs.removeValueForKey(e), i && delete this.root[e]) : delete this.cache[e], this.persistCache()
+ }
+
+ notify(e, t = "", u = "", h = {}) {
+ const c = h["open-url"], l = h["media-url"], a = u + (c ? `\n点击跳转: ${c}` : "") + (l ? `\n多媒体: ${l}` : "");
+ if (s && $notify(e, t, u, h), n && $notification.post(e, t, a), o && $notification.post(e, t, u, c), i) if (r) {
+ require("push").schedule({title: e, body: (t ? t + "\n" : "") + a})
+ } else console.log(`${e}\n${t}\n${a}\n\n`)
+ }
+
+ log(e) {
+ this.debug && console.log(e)
+ }
+
+ info(e) {
+ console.log(e)
+ }
+
+ error(e) {
+ console.log("ERROR: " + e)
+ }
+
+ wait(e) {
+ return new Promise(t => setTimeout(t, e))
+ }
+
+ done(e = {}) {
+ s || o || n ? $done(e) : i && !r && "undefined" != typeof $context && ($context.headers = e.headers, $context.statusCode = e.statusCode, $context.body = e.body)
+ }
+ }(e, t)
+}
+
+/*********************************** Mini Express *************************************/
+function express(){const t=[],e=["GET","POST","PUT","DELETE","PATCH","OPTIONS","HEAD'","ALL"],n=(e,s,h=0)=>{const{path:u,query:l}=function(t){const e=(t.match(/https?:\/\/[^\/]+(\/[^?]*)/)||[])[1]||"/",n=t.indexOf("?"),s={};if(-1!==n){let e=t.slice(t.indexOf("?")+1).split("&");for(let t=0;t{n(e,s,a)},r={method:e,url:s,path:u,query:l,params:i(f.pattern,u)},h=o();f.callback(r,h,t)}else{o().status("404").send("ERROR: 404 not found")}},s={};return e.forEach(e=>{s[e.toLowerCase()]=((n,s)=>{t.push({method:e,pattern:n,callback:s})})}),s.route=(n=>{const s={};return e.forEach(e=>{s[e.toLowerCase()]=(o=>(t.push({method:e,pattern:n,callback:o}),s))}),s}),s.start=(()=>{const{method:t,url:e}=$request;n(t,e)}),s;function o(){let t="200";const{isQX:e,isLoon:n,isSurge:s}=function(){const t="undefined"!=typeof $task,e="undefined"!=typeof $loon,n="undefined"!=typeof $httpClient&&!this.isLoon;return{isQX:t,isLoon:e,isSurge:n}}(),o={"Content-Type":"text/plain;charset=UTF-8"};return new class{status(e){return t=e,this}send(r=""){const i={status:t,body:r,headers:o};e?$done(...i):(n||s)&&$done({response:i})}end(){this.send()}html(t){this.set("Content-Type","text/html;charset=UTF-8"),this.send(t)}json(t){this.set("Content-Type","application/json;charset=UTF-8"),this.send(JSON.stringify(t))}set(t,e){return o[t]=e,this}}}function r(t,e){if(t instanceof RegExp&&t.test(e))return!0;if(-1===t.indexOf(":")){const n=e.split("/"),s=t.split("/");for(let t=0;t>> 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, "/"));
+ }
+}
\ No newline at end of file
diff --git a/root.json b/root.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/root.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file