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