Sub-Store/parser.js
2020-08-19 10:45:03 +08:00

2093 lines
70 KiB
JavaScript

const $ = API("sub-store");
// Constants
const SUBS_KEY = "subs";
// SOME INITIALIZATIONS
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
// BACKEND API
const $app = express();
$app.get("/v1/download/:name", downloadSub)
$app.route("/v1/sub/:name")
.get(getSub)
.patch(updateSub)
.delete(deleteSub);
$app.route("/v1/sub")
.get(getAllSubs)
.post(newSub)
.delete(deleteAllSubs);
$app.all("/", (req, res) => {
res.send("Hello from Sub-Store! Made with ❤️ by Peng-YM.")
});
$app.start();
// SOME CONSTANTS
const FALL_BACK_TARGET = "Loon";
const DEFAULT_SUPPORTED_PLATFORMS = {
QX: true,
Loon: true,
Surge: true,
Node: true
}
const AVAILABLE_FILTERS = {
"Keyword Filter": KeywordFilter,
"Discard Keyword Filter": DiscardKeywordFilter,
"Useless Filter": UselessFilter,
"Region Filter": RegionFilter,
"Regex Filter": RegexFilter,
"Discard Regex Filter": DiscardRegexFilter,
"Type Filter": TypeFilter,
"Script Filter": ScriptFilter
}
const AVAILABLE_OPERATORS = {
"Set Property Operator": SetPropertyOperator,
"Flag Operator": FlagOperator,
"Sort Operator": SortOperator,
"Keyword Sort Operator": KeywordSortOperator,
"Keyword Rename Operator": KeywordRenameOperator,
"Keyword Delete Operator": KeywordDeleteOperator,
"Regex Rename Operator": RegexRenameOperator,
"Regex Delete Operator": RegexDeleteOperator,
"Script Operator": ScriptOperator
}
/**************************** API -- Subscriptions ***************************************/
// download subscription, for APP only
async function downloadSub(req, res) {
const {name} = req.params;
const platform = getPlatformFromHeaders(req.headers);
console.log('=======================================')
console.log(`Downloading subscription: ${name}. Target platform ==> ${platform}\n`);
const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) {
const sub = allSubs[name];
// download from url
const raw = await $.http.get(sub.url).then(resp => resp.body).catch(err => {
$.notify('[Sub-Store]', '❌ 无法获取订阅!', `错误信息:${err}`)
res.status(500).json({
status: "failed",
message: err
});
});
const $parser = ProxyParser(platform);
let proxies = $parser.parse(raw);
// filters
const $filter = ProxyFilter();
// create filters from sub conf
const userFilters = [];
for (const item of sub.filters) {
const filter = AVAILABLE_FILTERS[item.type];
if (filter) {
userFilters.push(filter(...(item.args || [])));
console.log(`Filter "${item.type}" added. Arguments: ${item.args || "None"}`);
}
}
$filter.addFilters(...userFilters);
// operators
const $operator = ProxyOperator();
const userOperators = [];
for (const item of sub.operators) {
const operator = AVAILABLE_OPERATORS[item.type];
if (operator) {
userOperators.push(operator(...(item.args || [])));
console.log(`Operator "${item.type}" added. Arguments: ${item.args || "None"}`);
}
}
$operator.addOperators(...userOperators);
// process filters and operators
console.log("\nApplying filters...");
proxies = $filter.process(proxies);
console.log("\nApplying operators...");
proxies = $operator.process(proxies);
// convert to target platform and output
res.send($parser.produce(proxies));
} else {
res.status(404).json({
status: "failed",
message: `订阅${name}不存在!`
})
}
}
async function getSub(req, res) {
const {name} = req.params;
const sub = $.read(SUBS_KEY)[name];
if (sub) {
res.json({
status: "success",
data: sub
});
} else {
res.status(404).json({
status: "failed"
});
}
}
async function newSub(req, res) {
const sub = req.body;
const allSubs = $.read('subs');
if (allSubs[sub.name]) {
res.status(500).json({
status: "failed",
message: `订阅${sub.name}已存在!`
});
}
// validate name
if (/^[\w-_]*$/.test(sub.name)) {
allSubs[sub.name] = sub;
$.write(allSubs, 'subs');
res.status(201).json({
status: "success",
data: sub
});
} else {
res.status(500).json({
status: "failed",
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
})
}
}
async function updateSub(req, res) {
const {name} = req.params;
let sub = req.body;
const allSubs = $.read('subs');
if (allSubs[name]) {
const newSub = {
...allSubs[name],
...sub
};
allSubs[name] = newSub;
$.write(allSubs, 'subs');
res.json({
status: "success",
data: newSub
})
} else {
res.status(500).json({
status: "failed",
message: `订阅${name}不存在,无法更新!`
});
}
}
async function deleteSub(req, res) {
const {name} = req.params;
let allSubs = $.read(SUBS_KEY);
delete allSubs[name];
$.write(allSubs, "subs");
res.json({
status: "success"
});
}
async function getAllSubs(req, res) {
const allSubs = $.read(SUBS_KEY);
res.json({
status: "success",
data: Object.keys(allSubs)
});
}
async function deleteAllSubs(req, res) {
$.write({}, "subs");
res.json({
status: "success"
});
}
/**************************** Proxy Handlers ***************************************/
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, Node_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}
}
/**************************** 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
// 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(";");
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]);
// 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
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}${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,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 Node_Producer() {
const targetPlatform = "Node";
const output = (proxy) => {
return JSON.stringify(proxy);
}
return {targetPlatform, output};
}
/**************************** 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 Operator",
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 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 keywords
function KeywordSortOperator(...keywords) {
return {
name: "Keyword Sort Operator",
func: proxies => proxies.sort((a, b) => {
const oA = getKeywordOrder(keywords, a.name);
const oB = getKeywordOrder(keywords, 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 getKeywordOrder(keywords, str) {
let order = null;
for (let i = 0; i < keywords.length; i++) {
if (str.indexOf(keywords[i]) !== -1) {
order = i + 1; // plus 1 is important! 0 will be treated as false!!!
break;
}
}
return order;
}
// rename by keywords
// keywords: [{old: "old", now: "now"}]
function KeywordRenameOperator(...keywords) {
return {
name: "Keyword Rename Operator",
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) {
return {
name: "Regex Rename Operator",
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) {
const keywords_ = keywords.map(k => {
return {
old: k,
now: ""
}
})
return {
name: "Keyword Delete Operator",
func: KeywordRenameOperator(keywords_).func
}
}
// 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 func(proxies) {
// do something
return proxies;
}
WARNING:
1. This function name should be `func`!
2. Always declare variable before using it!
*/
function ScriptOperator(script, encoded = true) {
if (encoded) {
const Base64 = new Base64Code();
script = Base64.safeDecode(script);
}
return {
name: "Script Operator",
func: (proxies) => {
;(function () {
eval(script);
return func(proxies);
})();
}
}
}
/**************************** Filters ***************************************/
// filter by keywords
function KeywordFilter(...keywords) {
return {
name: "Keyword Filter",
func: (proxies) => {
return proxies.map(proxy => keywords.some(k => proxy.name.indexOf(k) !== -1));
}
}
}
function DiscardKeywordFilter(...keywords) {
return {
name: "Discard Keyword Filter",
func: proxies => {
const filter = KeywordFilter(keywords).func;
return NOT(filter(proxies));
}
}
}
// filter useless proxies
function UselessFilter() {
const KEYWORDS = ["流量", "时间", "应急", "过期", "Bandwidth", "expire"];
return {
name: "Useless Filter",
func: DiscardKeywordFilter(KEYWORDS).func
}
}
// filter by regions
function RegionFilter(...regions) {
const REGION_MAP = {
"HK": "🇭🇰",
"TW": "🇹🇼",
"US": "🇺🇸",
"SG": "🇸🇬",
"JP": "🇯🇵",
"UK": "🇬🇧",
"KR": "🇰🇷"
};
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) {
return {
name: "Regex Filter",
func: (proxies) => {
return proxies.map(proxy => regex.some(r => r.test(proxy.name)));
}
}
}
function DiscardRegexFilter(...regex) {
return {
name: "Discard Regex Filter",
func: proxies => {
const filter = RegexFilter(regex).func;
return NOT(filter(proxies));
}
}
}
// 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 variable before using it!
*/
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);
}
// UUID
// source: https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
function UUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// get platform form UA
function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = "";
for (let k of keys) {
if (k.match(/USER-AGENT/i)) {
UA = headers[k];
break;
}
}
if (UA.indexOf("Quantumult%20X") !== -1) {
return "QX";
} else if (UA.indexOf("Surge") !== -1) {
return "Surge";
} else if (UA.indexOf("Decar") !== -1) {
return "Loon";
} else {
return FALL_BACK_TARGET;
}
}
/*********************************** 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(port = 3000) {
const {isNode} = ENV();
// node support
if (isNode) {
const express_ = require("express");
const bodyParser = require("body-parser");
const app = express_();
app.use(bodyParser.json({verify: rawBodySaver}));
app.use(bodyParser.urlencoded({verify: rawBodySaver, extended: true}));
app.use(bodyParser.raw({verify: rawBodySaver, type: '*/*'}));
// adapter
app.start = () => {
app.listen(port, () => {
console.log(`Express started on port: ${port}`);
})
}
return app;
}
// route handlers
const handlers = [];
// http methods
const METHODS_NAMES = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS",
"HEAD'",
"ALL",
];
// dispatch url to route
const dispatch = (request, start = 0) => {
let {method, url, headers, body} = request;
method = method.toUpperCase();
const {path, query} = extractURL(url);
let handler = null;
let i;
for (i = start; i < handlers.length; i++) {
if (handlers[i].method === "ALL" || method === handlers[i].method) {
const {pattern} = handlers[i];
if (patternMatched(pattern, path)) {
handler = handlers[i];
break;
}
}
}
if (handler) {
// dispatch to next handler
const next = () => {
dispatch(method, url, i);
};
const req = {
method, url, path, query,
params: extractPathParams(handler.pattern, path),
headers, body
};
const res = Response();
handler.callback(req, res, next).catch(err => {
res.status(500).json({
status: "failed",
message: err
});
});
} else {
// no route, return 404
const res = Response();
res.status("404").json({
status: "failed",
message: "ERROR: 404 not found"
});
}
};
const app = {};
// attach http methods
METHODS_NAMES.forEach((method) => {
app[method.toLowerCase()] = (pattern, callback) => {
// add handler
handlers.push({method, pattern, callback});
};
});
// chainable route
app.route = (pattern) => {
const chainApp = {};
METHODS_NAMES.forEach((method) => {
chainApp[method.toLowerCase()] = (callback) => {
// add handler
handlers.push({method, pattern, callback});
return chainApp;
};
});
return chainApp;
};
// start service
app.start = () => {
dispatch($request);
};
return app;
/************************************************
Utility Functions
*************************************************/
function rawBodySaver(req, res, buf, encoding) {
if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8');
}
}
function Response() {
let statusCode = "200";
const {isQX, isLoon, isSurge} = ENV();
const headers = {
"Content-Type": "text/plain;charset=UTF-8",
};
return new (class {
status(code) {
statusCode = code;
return this;
}
send(body = "") {
const response = {
status: statusCode,
body,
headers,
};
if (isQX) {
$done(...response);
} else if (isLoon || isSurge) {
$done({
response,
});
}
}
end() {
this.send();
}
html(data) {
this.set("Content-Type", "text/html;charset=UTF-8");
this.send(data);
}
json(data) {
this.set("Content-Type", "application/json;charset=UTF-8");
this.send(JSON.stringify(data));
}
set(key, val) {
headers[key] = val;
return this;
}
})();
}
function patternMatched(pattern, path) {
if (pattern instanceof RegExp && pattern.test(path)) {
return true;
} else {
// root pattern, match all
if (pattern === "/") return true;
// normal string pattern
if (pattern.indexOf(":") === -1) {
const spath = path.split("/");
const spattern = pattern.split("/");
for (let i = 0; i < spattern.length; i++) {
if (spath[i] !== spattern[i]) {
return false;
}
}
return true;
}
// string pattern with path parameters
else if (extractPathParams(pattern, path)) {
return true;
}
}
return false;
}
function extractURL(url) {
// extract path
const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
const path = match[1] || "/";
// extract query string
const split = url.indexOf("?");
const query = {};
if (split !== -1) {
let hashes = url.slice(url.indexOf("?") + 1).split("&");
for (let i = 0; i < hashes.length; i++) {
hash = hashes[i].split("=");
query[hash[0]] = hash[1];
}
}
return {
path,
query,
};
}
function extractPathParams(pattern, path) {
if (pattern.indexOf(":") === -1) {
return null;
} else {
const params = {};
for (let i = 0, j = 0; i < pattern.length; i++, j++) {
if (pattern[i] === ":") {
let key = [];
let val = [];
while (pattern[++i] !== "/" && i < pattern.length) {
key.push(pattern[i]);
}
while (path[j] !== "/" && j < path.length) {
val.push(path[j++]);
}
params[key.join("")] = val.join("");
} else {
if (pattern[i] !== path[j]) {
return null;
}
}
}
return params;
}
}
}
/******************************** Base 64 *********************************************/
// Base64 Coding Library
// https://github.com/dankogai/js-base64#readme
// Under BSD License
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(/&gt;/g, ">").replace(/&lt;/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, "/"));
}
}