Sub-Store/backend/sub-store.js
2020-08-26 21:09:34 +08:00

2957 lines
98 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Sub-Store v0.1 (Backend only)
* @Author: Peng-YM
* @Description:
* 适用于QXLoonSurge的订阅管理工具。
* - 功能
* 1. 订阅转换支持SS, SSR, V2RayN, QX, Loon, Surge, Clash格式的互相转换。
* 2. 节点过滤,重命名,排序等。
* 3. 订阅拆分,组合。
*/
const $ = API("sub-store", true);
// Constants
const SUBS_KEY = "subs";
const COLLECTIONS_KEY = "collections";
const RESOURCE_CACHE_KEY = "resources";
// SOME INITIALIZATIONS
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
if (!$.read(RESOURCE_CACHE_KEY)) $.write({}, RESOURCE_CACHE_KEY);
// BACKEND API
const $app = express();
// download
$app.get("/download/:name", downloadSub);
$app.get("/download/collection/:name", downloadCollection);
// refresh
$app.post("/api/refresh", refreshResource);
// subscriptions
$app.route("/api/sub/:name")
.get(getSub)
.patch(updateSub)
.delete(deleteSub);
$app.route("/api/sub")
.get(getAllSubs)
.post(newSub)
.delete(deleteAllSubs);
// collections
$app.route("/api/collection/:name")
.get(getCollection)
.patch(updateCollection)
.delete(deleteCollection);
$app.route("/api/collection")
.get(getAllCollections)
.post(newCollection)
.delete(deleteAllCollections);
$app.all("/", (req, res) => {
res.send("Hello from Sub-Store! Made with ❤️ by Peng-YM.")
});
$app.start();
// SOME CONSTANTS
const DEFAULT_SUPPORTED_PLATFORMS = {
QX: true,
Loon: true,
Surge: true,
Raw: true
}
const AVAILABLE_FILTERS = {
"Keyword Filter": KeywordFilter,
"Discard Keyword Filter": DiscardKeywordFilter,
"Useless Filter": UselessFilter,
"Region Filter": RegionFilter,
"Regex Filter": RegexFilter,
"Discard Regex Filter": DiscardRegexFilter,
"Type Filter": TypeFilter,
"Script Filter": ScriptFilter
}
const AVAILABLE_OPERATORS = {
"Set Property Operator": SetPropertyOperator,
"Flag Operator": FlagOperator,
"Sort Operator": SortOperator,
"Keyword Sort Operator": KeywordSortOperator,
"Keyword Rename Operator": KeywordRenameOperator,
"Keyword Delete Operator": KeywordDeleteOperator,
"Regex Rename Operator": RegexRenameOperator,
"Regex Delete Operator": RegexDeleteOperator,
"Script Operator": ScriptOperator
}
/**************************** API -- Subscriptions ***************************************/
// refresh resource
async function refreshResource(req, res) {
const {url} = req.body;
const cachedResources = $.read(RESOURCE_CACHE_KEY);
cachedResources[url] = await $.http.get(url).then(resp => resp => resp.body).catch(err => {
res.status(500).json({
status: "failed",
message: `Cannot refresh remote resource: ${url}\n Reason: ${err}`
});
});
$.write(cachedResources, cachedResources);
res.json({
status: "success"
});
}
// download subscription, for APP only
async function downloadSub(req, res) {
const {name} = req.params;
const platform = getPlatformFromHeaders(req.headers);
const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) {
const sub = allSubs[name];
try {
const output = await parseSub(sub, platform);
res.send(output);
} catch (err) {
res.status(500).json({
status: "failed",
message: err
});
}
} else {
res.status(404).json({
status: "failed",
message: `订阅${name}不存在!`
});
}
}
async function parseSub(sub, platform) {
let raw;
const cachedResources = $.read(RESOURCE_CACHE_KEY);
if (platform === "Raw") {
// use cache if available
raw = cachedResources[sub.url];
}
// always download from url
raw = raw || await $.http.get(sub.url).then(resp => resp.body).catch(err => {
throw new Error(err);
});
cachedResources[sub.url] = raw;
$.write(cachedResources, RESOURCE_CACHE_KEY);
$.log("=======================================================================");
$.log(`Processing subscription: ${sub.name}, target platform ==> ${platform}.`);
const $parser = ProxyParser(platform);
let proxies = $parser.parse(raw);
// filters
const $filter = ProxyFilter();
// operators
const $operator = ProxyOperator();
for (const item of sub.process || []) {
// process script
if (item.type.indexOf("Script") !== -1) {
if (item.args && item.args[0].indexOf("http") !== -1) {
// if this is remote script
item.args[0] = await $.http.get(item.args[0]).then(resp => resp.body).catch(err => {
throw new Error(`Error when downloading remote script: ${item.args[0]}.\n Reason: ${err}`);
});
}
}
if (item.type.indexOf("Filter") !== -1) {
const filter = AVAILABLE_FILTERS[item.type];
if (filter) {
$filter.addFilters(filter(...(item.args || [])));
proxies = $filter.process(proxies);
$.log(`Applying filter "${item.type}" with arguments:\n >>> ${item.args || "None"}`);
}
} else if (item.type.indexOf("Operator") !== -1) {
const operator = AVAILABLE_OPERATORS[item.type];
if (operator) {
$operator.addOperators(operator(...(item.args || [])));
proxies = $operator.process(proxies);
$.log(`Applying operator "${item.type}" with arguments: \n >>> ${item.args || "None"}`);
}
}
}
return $parser.produce(proxies);
}
// Subscriptions
async function getSub(req, res) {
const {name} = req.params;
const sub = $.read(SUBS_KEY)[name];
if (sub) {
res.json({
status: "success",
data: sub
});
} else {
res.status(404).json({
status: "failed",
message: `未找到订阅:${name}!`
});
}
}
async function newSub(req, res) {
const sub = req.body;
const allSubs = $.read(SUBS_KEY);
if (allSubs[sub.name]) {
res.status(500).json({
status: "failed",
message: `订阅${sub.name}已存在!`
});
}
// validate name
if (/^[\w-_]*$/.test(sub.name)) {
allSubs[sub.name] = sub;
$.write(allSubs, SUBS_KEY);
res.status(201).json({
status: "success",
data: sub
});
} else {
res.status(500).json({
status: "failed",
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
})
}
}
async function updateSub(req, res) {
const {name} = req.params;
let sub = req.body;
const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) {
const newSub = {
...allSubs[name],
...sub
};
// allow users to update the subscription name
if (name !== sub.name) {
// we need to find out all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) {
const idx = allCols[k].subscriptions.indexOf(name);
if (idx !== -1) {
allCols[k].subscriptions[idx] = sub.name;
}
}
// update subscriptions
delete allSubs[name];
allSubs[sub.name] = newSub;
} else {
allSubs[name] = newSub;
}
$.write(allSubs, SUBS_KEY);
res.json({
status: "success",
data: newSub
})
} else {
res.status(500).json({
status: "failed",
message: `订阅${name}不存在,无法更新!`
});
}
}
async function deleteSub(req, res) {
const {name} = req.params;
// delete from subscriptions
let allSubs = $.read(SUBS_KEY);
delete allSubs[name];
$.write(allSubs, SUBS_KEY);
// delete from collections
let allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) {
allCols[k].subscriptions = allCols[k].subscriptions.filter(s => s !== name);
}
$.write(allCols, COLLECTIONS_KEY);
res.json({
status: "success"
});
}
async function getAllSubs(req, res) {
const allSubs = $.read(SUBS_KEY);
res.json({
status: "success",
data: allSubs
});
}
async function deleteAllSubs(req, res) {
$.write({}, SUBS_KEY);
res.json({
status: "success"
});
}
// Collections
async function downloadCollection(req, res) {
const {name} = req.params;
const collection = $.read(COLLECTIONS_KEY)[name];
const platform = getPlatformFromHeaders(req.headers);
if (collection) {
const subs = collection.subscriptions || [];
const output = await Promise.all(subs.map(async id => {
const sub = $.read(SUBS_KEY)[id];
try {
return parseSub(sub, platform);
} catch (err) {
console.log(`ERROR when process subscription: ${id}`);
return "";
}
}));
res.send(output.join("\n"));
} else {
$.notify('[Sub-Store]', `❌ 未找到订阅集:${name}`)
res.status(404).json({
status: "failed",
message: `❌ 未找到订阅集:${name}`
});
}
}
async function getCollection(req, res) {
const {name} = req.params;
const collection = $.read(COLLECTIONS_KEY)[name];
if (collection) {
res.json({
status: "success",
data: collection
});
} else {
res.status(404).json({
status: "failed",
message: `未找到订阅集:${name}!`
});
}
}
async function newCollection(req, res) {
const collection = req.body;
const allCol = $.read(COLLECTIONS_KEY);
if (allCol[collection.name]) {
res.status(500).json({
status: "failed",
message: `订阅集${collection.name}已存在!`
});
}
// validate name
if (/^[\w-_]*$/.test(collection.name)) {
allCol[collection.name] = collection;
$.write(allCol, COLLECTIONS_KEY);
res.status(201).json({
status: "success",
data: collection
});
} else {
res.status(500).json({
status: "failed",
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
})
}
}
async function updateCollection(req, res) {
const {name} = req.params;
let collection = req.body;
const allCol = $.read(COLLECTIONS_KEY);
if (allCol[name]) {
const newCol = {
...allCol[name],
...collection
};
// allow users to update collection name
delete allCol[name];
allCol[collection.name || name] = newCol;
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: "success",
data: newCol
})
} else {
res.status(500).json({
status: "failed",
message: `订阅集${name}不存在,无法更新!`
});
}
}
async function deleteCollection(req, res) {
const {name} = req.params;
let allCol = $.read(COLLECTIONS_KEY);
delete allCol[name];
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: "success"
});
}
async function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
res.json({
status: "success",
data: allCols
});
}
async function deleteAllCollections(req, res) {
$.write({}, COLLECTIONS_KEY);
res.json({
status: "success"
});
}
/**************************** Proxy Handlers ***************************************/
function ProxyParser(targetPlatform) {
// parser collections
const parsers = [];
const producers = [];
function addParsers(...args) {
args.forEach(a => parsers.push(a()));
}
function addProducers(...args) {
args.forEach(a => producers.push(a()))
}
function parse(raw) {
raw = preprocessing(raw);
const lines = raw.split("\n");
const result = [];
// convert to json format
for (let line of lines) {
line = line.trim();
if (line.length === 0) continue; // skip empty line
if (line.startsWith("#")) continue; // skip comments
let matched = false;
for (const p of parsers) {
const {patternTest, func} = p;
// some lines with weird format may produce errors!
let patternMatched;
try {
patternMatched = patternTest(line);
} catch (err) {
patternMatched = false;
}
if (patternMatched) {
matched = true;
// run parser safely.
try {
const proxy = func(line);
if (!proxy) {
// failed to parse this line
console.log(`ERROR: parser return nothing for \n${line}\n`);
break;
}
// skip unsupported proxies
// if proxy.supported is undefined, assume that all platforms are supported.
if (typeof proxy.supported === 'undefined' || proxy.supported[targetPlatform]) {
result.push(proxy);
break;
}
} catch (err) {
console.log(`ERROR: Failed to parse line: \n ${line}\n Reason: ${err}`);
}
}
}
if (!matched) {
console.log(`ERROR: Failed to find a rule to parse line: \n${line}\n`);
}
}
return result;
}
function produce(proxies) {
for (const p of producers) {
if (p.targetPlatform === targetPlatform) {
return proxies.map(proxy => {
try {
return p.output(proxy);
} catch (err) {
console.log(`ERROR: cannot produce proxy: ${JSON.stringify(proxy)}\nReason: ${err}`);
return "";
}
}).join("\n");
}
}
throw new Error(`Cannot find any producer for target platform: ${targetPlatform}`);
}
// preprocess raw input
function preprocessing(raw) {
let output;
if (raw.indexOf("DOCTYPE html") !== -1) {
// HTML format, maybe a wrong URL!
throw new Error("Invalid format HTML!");
} else if (raw.indexOf("proxies") !== -1) {
// Clash YAML format
// codes are modified from @KOP-XIAO
// https://github.com/KOP-XIAO/QuantumultX
if (raw.indexOf("{") !== -1) {
raw = raw.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");
const proxies = YAML.eval(raw).proxies;
output = proxies.map(p => JSON.stringify(p));
} else {
// check if content is based64 encoded
const Base64 = new Base64Code();
const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v"];
if (keys.some(k => raw.indexOf(k) !== -1)) {
output = Base64.safeDecode(raw);
} else {
output = raw;
}
output = output.split("\n");
for (let i = 0; i < output.length; i++) {
output[i] = output[i].trim(); // trim lines
}
}
return output.join("\n");
}
// Parsers
addParsers(
Clash_All,
// URI format parsers
URI_SS, URI_SSR, URI_VMess, URI_Trojan,
// Quantumult X platform
QX_SS, QX_SSR, QX_VMess, QX_Trojan, QX_Http,
// Loon platform
Loon_SS, Loon_SSR, Loon_VMess, Loon_Trojan, Loon_Http,
// Surge platform
Surge_SS, Surge_VMess, Surge_Trojan, Surge_Http
);
// Producers
addProducers(
QX_Producer, Loon_Producer, Surge_Producer, Raw_Producer
);
return {
parse, produce
};
}
function ProxyFilter() {
const filters = [];
function addFilters(...args) {
args.forEach(a => filters.push(a));
}
// select proxies
function process(proxies) {
let selected = FULL(proxies.length, true);
for (const filter of filters) {
try {
selected = AND(selected, filter.func(proxies));
} catch (err) {
console.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
}
}
return proxies.filter((_, i) => selected[i])
}
return {
process, addFilters
}
}
function ProxyOperator() {
const operators = [];
function addOperators(...args) {
args.forEach(a => operators.push(a));
}
// run all operators
function process(proxies) {
let output = clone(proxies);
for (const op of operators) {
try {
const output_ = op.func(output);
if (output_) output = output_;
} catch (err) {
// print log and skip this operator
console.log(`ERROR: cannot apply operator ${op.name}! Reason: ${err}`);
}
}
return output;
}
return {addOperators, process}
}
/**************************** URI Format ***************************************/
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
function URI_SS() {
const patternTest = (line) => {
return /^ss:\/\//.test(line);
}
const Base64 = new Base64Code();
const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
const func = (line) => {
// parse url
let content = line.split("ss://")[1];
const proxy = {
name: decodeURIComponent(line.split("#")[1]),
type: "ss",
supported
}
content = content.split("#")[0]; // strip proxy name
// handle IPV4 and IPV6
const serverAndPort = content.match(/@([^\/]*)\//)[1];
const portIdx = serverAndPort.lastIndexOf(":");
proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = serverAndPort.substring(portIdx + 1);
const userInfo = Base64.safeDecode(content.split("@")[0]).split(":");
proxy.cipher = userInfo[0];
proxy.password = userInfo[1];
// handle obfs
const idx = content.indexOf("?plugin=");
if (idx !== -1) {
const pluginInfo = ("plugin=" + decodeURIComponent(content.split("?plugin=")[1])).split(";");
const params = {};
for (const item of pluginInfo) {
const [key, val] = item.split("=");
if (key) params[key] = val || true; // some options like "tls" will not have value
}
switch (params.plugin) {
case 'simple-obfs':
proxy.plugin = 'obfs'
proxy['plugin-opts'] = {
mode: params.obfs,
host: params['obfs-host']
}
break
case 'v2ray-plugin':
proxy.supported = {
...DEFAULT_SUPPORTED_PLATFORMS,
Loon: false,
Surge: false
}
proxy.obfs = 'v2ray-plugin'
proxy['plugin-opts'] = {
mode: "websocket",
host: params['obfs-host'],
path: params.path || ""
}
break
default:
throw new Error(`Unsupported plugin option: ${params.plugin}`)
}
}
return proxy;
}
return {patternTest, func};
}
// Parse URI SSR format, such as ssr://xxx
function URI_SSR() {
const patternTest = (line) => {
return /^ssr:\/\//.test(line);
}
const Base64 = new Base64Code();
const supported = {
...DEFAULT_SUPPORTED_PLATFORMS,
Surge: false
}
const func = (line) => {
line = Base64.safeDecode(line.split("ssr://")[1]);
// handle IPV6 & IPV4 format
let splitIdx = line.indexOf(':origin');
if (splitIdx === -1) {
splitIdx = line.indexOf(":auth_");
}
const serverAndPort = line.substring(0, splitIdx);
const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
const port = serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1);
let params = line.substring(splitIdx + 1).split("/?")[0].split(":");
let proxy = {
type: "ssr",
server,
port,
protocol: params[0],
cipher: params[1],
obfs: params[2],
password: Base64.safeDecode(params[3]),
supported
}
// get other params
params = {};
line = line.split("/?")[1].split("&");
if (line.length > 1) {
for (const item of line) {
const [key, val] = item.split("=");
params[key] = val;
}
}
proxy = {
...proxy,
name: Base64.safeDecode(params.remarks),
"protocol-param": Base64.safeDecode(params.protoparam).replace(/\s/g, "") || "",
"obfs-param": Base64.safeDecode(params.obfsparam).replace(/\s/g, "") || ""
}
return proxy;
}
return {patternTest, func};
}
// V2rayN URI VMess format
// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
function URI_VMess() {
const patternTest = (line) => {
return /^vmess:\/\//.test(line);
}
const Base64 = new Base64Code();
const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
const func = (line) => {
line = line.split("vmess://")[1];
const params = JSON.parse(Base64.safeDecode(line));
const proxy = {
name: params.ps,
type: "vmess",
server: params.add,
port: params.port,
cipher: "auto", // V2rayN has no default cipher! use aes-128-gcm as default.
uuid: params.id,
alterId: params.aid || 0,
tls: JSON.parse(params.tls || "false"),
supported
}
// handle obfs
if (params.net === 'ws') {
proxy.network = 'ws';
proxy['ws-path'] = params.path;
proxy['ws-headers'] = {
Host: params.host || params.add
}
}
return proxy
}
return {patternTest, func};
}
// Trojan URI format
function URI_Trojan() {
const patternTest = (line) => {
return /^trojan:\/\//.test(line);
}
const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
const func = (line) => {
// trojan forces to use 443 port
if (line.indexOf(":443") === -1) {
throw new Error("Trojan port should always be 443!");
}
line = line.split("trojan://")[1];
const server = line.split("@")[1].split(":443")[0];
return {
name: `[Trojan] ${server}`, // trojan uri has no server tag!
type: "trojan",
server,
port: 443,
password: line.split("@")[0],
supported
}
}
return {patternTest, func};
}
/**************************** Clash ***************************************/
function Clash_All() {
const patternTest = (line) => {
return line.indexOf("{") !== -1;
}
const func = line => JSON.parse(line);
return {patternTest, func};
}
/**************************** Quantumult X ***************************************/
function QX_SS() {
const patternTest = (line) => {
return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") === -1;
};
const func = (line) => {
const params = getQXParams(line);
const proxy = {
name: params.tag,
type: "ss",
server: params.server,
port: params.port,
cipher: params.method,
password: params.password,
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
supported: clone(DEFAULT_SUPPORTED_PLATFORMS)
};
// handle obfs options
if (params.obfs) {
proxy["plugin-opts"] = {
host: params['obfs-host'] || proxy.server
};
switch (params.obfs) {
case "http":
case "tls":
proxy.plugin = "obfs";
proxy["plugin-opts"].mode = params.obfs;
break;
case "ws":
case "wss":
proxy["plugin-opts"] = {
...proxy["plugin-opts"],
mode: "websocket",
path: params['obfs-uri'],
tls: params.obfs === 'wss'
}
proxy.plugin = "v2ray-plugin"
// Surge and Loon lack support for v2ray-plugin obfs
proxy.supported.Surge = false
proxy.supported.Loon = false
break;
}
}
return proxy;
};
return {patternTest, func};
}
function QX_SSR() {
const patternTest = (line) => {
return /^shadowsocks\s*=/.test(line.split(",")[0].trim()) && line.indexOf("ssr-protocol") !== -1;
};
const supported = {
...DEFAULT_SUPPORTED_PLATFORMS,
Surge: false
}
const func = (line) => {
const params = getQXParams(line);
const proxy = {
name: params.tag,
type: "ssr",
server: params.server,
port: params.port,
cipher: params.method,
password: params.password,
protocol: params["ssr-protocol"],
obfs: "plain", // default obfs
"protocol-param": params['ssr-protocol-param'],
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
supported
}
// handle obfs options
if (params.obfs) {
proxy.obfs = params.obfs;
proxy['obfs-param'] = params['obfs-host']
}
return proxy;
}
return {patternTest, func};
}
function QX_VMess() {
const patternTest = (line) => {
return /^vmess\s*=/.test(line.split(",")[0].trim());
};
const func = (line) => {
const params = getQXParams(line)
const proxy = {
type: "vmess",
name: params.tag,
server: params.server,
port: params.port,
cipher: params.method || 'none',
uuid: params.password,
alterId: 0,
tls: params.obfs === 'over-tls' || params.obfs === 'wss',
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
}
if (proxy.tls) {
proxy.sni = params['obfs-host'] || params.server;
proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
}
// handle ws headers
if (params.obfs === 'ws' || params.obfs === 'wss') {
proxy.network = 'ws';
proxy['ws-path'] = params['obfs-uri'];
proxy['ws-headers'] = {
Host: params['obfs-host'] || params.server // if no host provided, use the same as server
}
}
return proxy;
}
return {patternTest, func};
}
function QX_Trojan() {
const patternTest = (line) => {
return /^trojan\s*=/.test(line.split(",")[0].trim());
};
const func = (line) => {
const params = getQXParams(line);
const proxy = {
type: "trojan",
name: params.tag,
server: params.server,
port: params.port,
password: params.password,
sni: params['tls-host'] || params.server,
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
}
proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
return proxy;
}
return {patternTest, func}
}
function QX_Http() {
const patternTest = (line) => {
return /^http\s*=/.test(line.split(",")[0].trim());
};
const func = (line) => {
const params = getQXParams(line);
const proxy = {
type: "http",
name: params.tag,
server: params.server,
port: params.port,
username: params.username,
password: params.password,
tls: JSON.parse(params['over-tls'] || "false"),
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
}
if (proxy.tls) {
proxy.sni = params['tls-host'] || proxy.server;
proxy.scert = !JSON.parse(params['tls-verification'] || 'true');
}
return proxy;
}
return {patternTest, func};
}
function getQXParams(line) {
const groups = line.split(",");
const params = {};
const protocols = ["shadowsocks", "vmess", "http", "trojan"];
groups.forEach((g) => {
const [key, value] = g.split("=");
if (protocols.indexOf(key) !== -1) {
params.type = key;
const conf = value.split(":");
params.server = conf[0];
params.port = conf[1];
} else {
params[key.trim()] = value.trim();
}
});
return params;
}
/**************************** Loon ***************************************/
function Loon_SS() {
const patternTest = (line) => {
return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocks';
}
const func = (line) => {
const params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "ss",
server: params[1],
port: params[2],
cipher: params[3],
password: params[4].replace(/"/g, "")
}
// handle obfs
if (params.length > 5) {
proxy.plugin = 'obfs';
proxy['plugin-opts'] = {
mode: proxy.obfs,
host: params[6]
}
}
return proxy;
}
return {patternTest, func};
}
function Loon_SSR() {
const patternTest = (line) => {
return line.split(",")[0].split("=")[1].trim().toLowerCase() === 'shadowsocksr';
}
const func = (line) => {
const params = line.split("=")[1].split(",");
const supported = clone(DEFAULT_SUPPORTED_PLATFORMS);
supported.Surge = false;
return {
name: line.split("=")[0].trim(),
type: "ssr",
server: params[1],
port: params[2],
cipher: params[3],
password: params[4].replace(/"/g, ""),
protocol: params[5],
"protocol-param": params[6].match(/{(.*)}/)[1],
supported,
obfs: params[7],
'obfs-param': params[8].match(/{(.*)}/)[1]
}
}
return {patternTest, func};
}
function Loon_VMess() {
const patternTest = (line) => {
// distinguish between surge vmess
return /^.*=\s*vmess/i.test(line.split(",")[0]) && line.indexOf("username") === -1;
}
const func = (line) => {
let params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "vmess",
server: params[1],
port: params[2],
cipher: params[3] || 'none',
uuid: params[4].replace(/"/g, ""),
alterId: 0,
}
// get transport options
params = params.splice(5);
for (const item of params) {
const [key, val] = item.split(":");
params[key] = val;
}
proxy.tls = JSON.parse(params['over-tls'] || 'false');
if (proxy.tls) {
proxy.sni = params['tls-name'] || proxy.server;
proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
}
switch (params.transport) {
case "tcp":
break;
case "ws":
proxy.network = params.transport
proxy['ws-path'] = params.path
proxy['ws-headers'] = {
Host: params.host
}
}
if (proxy.tls) {
proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false')
}
return proxy;
}
return {patternTest, func};
}
function Loon_Trojan() {
const patternTest = (line) => {
return /^.*=\s*trojan/i.test(line.split(",")[0]) && line.indexOf("password") === -1;
}
const func = (line) => {
const params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "trojan",
server: params[1],
port: params[2],
password: params[3].replace(/"/g, ""),
sni: params[1], // default sni is the server itself
scert: JSON.parse(params['skip-cert-verify'] || 'false')
}
// trojan sni
if (params.length > 4) {
const [key, val] = params[4].split(":");
if (key === 'tls-name') proxy.sni = val;
else throw new Error(`ERROR: unknown option ${key} for line: \n${line}`);
}
return proxy;
}
return {patternTest, func}
}
function Loon_Http() {
const patternTest = (line) => {
return /^.*=\s*http/i.test(line.split(",")[0])
&& line.split(",").length === 5
&& line.indexOf("username") === -1
&& line.indexOf("password") === -1
}
const func = (line) => {
const params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "http",
server: params[1],
port: params[2],
tls: params[2] === "443", // port 443 is considered as https type
username: (params[3] || "").replace(/"/g, ""),
password: (params[4] || "").replace(/"/g, "")
}
if (proxy.tls) {
proxy.sni = params['tls-name'] || proxy.server;
proxy.scert = JSON.parse(params['skip-cert-verify'] || 'false');
}
return proxy;
}
return {patternTest, func}
}
/**************************** Surge ***************************************/
function Surge_SS() {
const patternTest = (line) => {
return /^.*=\s*ss/.test(line.split(",")[0]);
}
const func = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "ss",
server: params.server,
port: params.port,
cipher: params['encrypt-method'],
password: params.password,
tfo: JSON.parse(params.tfo || "false"),
udp: JSON.parse(params['udp-relay'] || "false"),
}
// handle obfs
if (params.obfs) {
proxy.plugin = 'obfs';
proxy['plugin-opts'] = {
mode: params.obfs,
host: params['obfs-host']
}
}
return proxy;
}
return {patternTest, func}
}
function Surge_VMess() {
const patternTest = (line) => {
return /^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1;
}
const func = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "vmess",
server: params.server,
port: params.port,
uuid: params.username,
alterId: 0, // surge does not have this field
cipher: "none", // surge does not have this field
tls: JSON.parse(params.tls || "false"),
tfo: JSON.parse(params.tfo || "false"),
}
if (proxy.tls) {
proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
proxy.sni = params['sni'] || params.server;
}
// use websocket
if (JSON.parse(params.ws || "false")) {
proxy.network = 'ws';
proxy['ws-path'] = params['ws-path'];
proxy['ws-headers'] = {
Host: params.sni
}
}
return proxy;
}
return {patternTest, func};
}
function Surge_Trojan() {
const patternTest = (line) => {
return /^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1;
}
const func = (line) => {
const params = getSurgeParams(line);
return {
name: params.name,
type: "trojan",
server: params.server,
port: params.port,
password: params.password,
sni: params.sni || params.server,
tfo: JSON.parse(params.tfo || "false"),
scert: JSON.parse(params['skip-cert-verify'] || "false"),
}
}
return {patternTest, func};
}
function Surge_Http() {
const patternTest = (line) => {
return /^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().patternTest(line)
}
const func = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "http",
server: params.server,
port: params.port,
tls: JSON.parse(params.tls || "false"),
tfo: JSON.parse(params.tfo || "false"),
}
if (proxy.tls) {
proxy.scert = JSON.parse(params['skip-cert-verify'] || "false");
proxy.sni = params.sni || params.server;
}
if (params.username !== 'none') proxy.username = params.username;
if (params.password !== 'none') proxy.password = params.password;
return proxy;
}
return {patternTest, func}
}
function getSurgeParams(line) {
const params = {};
params.name = line.split("=")[0].trim();
const segments = line.split(",");
params.server = segments[1].trim();
params.port = segments[2].trim();
for (let i = 3; i < segments.length; i++) {
const item = segments[i]
if (item.indexOf("=") !== -1) {
const [key, value] = item.split("=");
params[key.trim()] = value.trim();
}
}
return params;
}
/**************************** Output Functions ***************************************/
function QX_Producer() {
const targetPlatform = "QX";
const output = (proxy) => {
let obfs_opts;
let tls_opts;
switch (proxy.type) {
case 'ss':
obfs_opts = "";
if (proxy.plugin === 'obfs') {
obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`;
}
if (proxy.plugin === 'v2ray-plugin') {
const {tls, host, path} = proxy['plugin-opts'];
obfs_opts = `,obfs=${tls ? 'wss' : 'ws'},obfs-host=${host}${path ? ',obfs-uri=' + path : ""}`;
}
return `shadowsocks = ${proxy.server}:${proxy.port}, method=${proxy.cipher}, password=${proxy.password}${obfs_opts}${proxy.tfo ? ", fast-open=true" : ", fast-open=false"}${proxy.udp ? ", udp-relay=true" : ", udp-relay=false"}, tag=${proxy.name}`
case 'ssr':
return `shadowsocks=${proxy.server}:${proxy.port},method=${proxy.cipher},password=${proxy.password},ssr-protocol=${proxy.protocol}${proxy['protocol-param'] ? ",ssr-protocol-param=" + proxy['protocol-param'] : ""}${proxy.obfs ? ",obfs=" + proxy.obfs : ""}${proxy['obfs-param'] ? ",obfs-host=" + proxy['obfs-param'] : ""}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
case 'vmess':
obfs_opts = "";
if (proxy.network === 'ws') {
// websocket
if (proxy.tls) {
// ws-tls
obfs_opts = `,obfs=wss,obfs-host=${proxy.sni}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""},tls-verification=${proxy.scert ? "false" : "true"}`;
} else {
// ws
obfs_opts = `,obfs=ws,obfs-host=${proxy['ws-headers'].Host}${proxy['ws-path'] ? ",obfs-uri=" + proxy['ws-path'] : ""}`;
}
} else {
// tcp
if (proxy.tls) {
obfs_opts = `,obfs=over-tls,obfs-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}`;
}
}
return `vmess=${proxy.server}:${proxy.port},method=${proxy.cipher},password=${proxy.uuid}${obfs_opts}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
case 'trojan':
return `trojan=${proxy.server}:${proxy.port},password=${proxy.password},tls-host=${proxy.sni},tls-verification=${proxy.scert ? "false" : "true"}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${proxy.name}`
case 'http':
tls_opts = "";
if (proxy.tls) {
tls_opts = `,over-tls=true,tls-verification=${proxy.scert ? "false" : "true"},tls-host=${proxy.sni}`;
}
return `http=${proxy.server}:${proxy.port},username=${proxy.username},password=${proxy.password}${tls_opts}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"},tag=${proxy.name}`;
}
throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
}
return {targetPlatform, output};
}
function Loon_Producer() {
const targetPlatform = "Loon";
const output = (proxy) => {
let obfs_opts, tls_opts;
switch (proxy.type) {
case "ss":
obfs_opts = ",,";
if (proxy.plugin === 'obfs') {
const {mode, host} = proxy['plugin-opts'];
obfs_opts = `,${mode},${host}`
}
return `${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password}${obfs_opts}`;
case "ssr":
return `${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},${proxy.password},${proxy.protocol},{${proxy['protocol-param']}},${proxy.obfs},{${proxy['obfs-param']}}`
case "vmess":
obfs_opts = "";
if (proxy.network === 'ws') {
const host = proxy['ws-headers'].Host;
obfs_opts = `,transport:ws,host:${host},path:${proxy['ws-path']}`;
} else {
obfs_opts = `,transport:tcp`;
}
if (proxy.tls) {
obfs_opts += `,tls-name=${proxy.sni},skip-cert-verify:${proxy.scert}`;
}
return `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},over-tls:${proxy.tls}${obfs_opts}`;
case "trojan":
return `${proxy.name}=trojan,${proxy.server},${proxy.port},${proxy.password},tls-name:${proxy.sni},skip-cert-verify:${proxy.scert}`;
case "http":
tls_opts = "";
const base = `${proxy.name}=${proxy.tls ? 'http' : 'https'},${proxy.server},${proxy.port},${proxy.username || ""},${proxy.password || ""}`;
if (proxy.tls) {
// https
tls_opts = `,skip-cert-verify:${proxy.scert},tls-name:${proxy.sni}`;
return base + tls_opts;
} else return base;
}
throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
}
return {targetPlatform, output}
}
function Surge_Producer() {
const targetPlatform = "Surge";
const output = (proxy) => {
let obfs_opts, tls_opts;
switch (proxy.type) {
case 'ss':
obfs_opts = "";
if (proxy.plugin === "obfs") {
obfs_opts = `,obfs=${proxy['plugin-opts'].mode},obfs-host=${proxy['plugin-opts'].host}`
} else {
throw new Error(`Platform ${targetPlatform} does not support obfs option: ${proxy.obfs}`);
}
return `${proxy.name}=ss,${proxy.server},${proxy.port},encrypt-method=${proxy.cipher},password=${proxy.password}${obfs_opts},tfo=${proxy.tfo || 'false'},udp-relay=${proxy.udp || 'false'}`;
case 'vmess':
tls_opts = "";
let config = `${proxy.name}=vmess,${proxy.server},${proxy.port},username=${proxy.uuid},tls=${proxy.tls},tfo=${proxy.tfo || "false"}`;
if (proxy.network === 'ws') {
const path = proxy['ws-path'];
const host = proxy['ws-headers'].Host;
config += `,ws=true${path ? ',ws-path=' + path : ""}${host ? ',ws-headers=HOST:' + host : ""}`;
}
if (proxy.tls) {
config += `,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
}
return config;
case 'trojan':
return `${proxy.name}=trojan,${proxy.server},${proxy.port},password=${proxy.password},sni=${proxy.sni},tfo=${proxy.tfo || 'false'}`;
case 'http':
tls_opts = ",tls=false";
if (proxy.tls) {
tls_opts = `,tls=true,skip-cert-verify=${proxy.scert},sni=${proxy.sni}`;
}
return `${proxy.name}=http,${proxy.server},${proxy.port}${proxy.username ? ",username=" + proxy.username : ""}${proxy.password ? ",password=" + proxy.password : ""}${tls_opts},tfo=${proxy.tfo || 'false'}`;
}
throw new Error(`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`);
}
return {targetPlatform, output};
}
function Raw_Producer() {
const targetPlatform = "Raw";
const output = (proxy) => {
return JSON.stringify(proxy);
}
return {targetPlatform, output};
}
/**************************** Operators ***************************************/
// force to set some properties (e.g., scert, udp, tfo, etc.)
function SetPropertyOperator(key, val) {
return {
name: "Set Property Operator",
func: proxies => {
return proxies.map(p => {
p[key] = val;
return p;
})
}
}
}
// add or remove flag for proxies
function FlagOperator(type = 1) {
return {
name: "Flag Operator",
func: proxies => {
return proxies.map(proxy => {
switch (type) {
case 0:
// no flag
proxy.name = removeFlag(proxy.name);
break
case 1:
// get flag
const newFlag = getFlag(proxy.name);
// remove old flag
proxy.name = removeFlag(proxy.name);
proxy.name = newFlag + " " + proxy.name;
proxy.name = proxy.name.replace(/🇹🇼/g, "🇨🇳");
break;
default:
throw new Error("Unknown flag type: " + type);
}
return proxy;
})
}
}
}
// sort proxies according to their names
function SortOperator(order = 'asc') {
return {
name: "Sort Operator",
func: proxies => {
switch (order) {
case "asc":
case 'desc':
return proxies.sort((a, b) => {
let res = (a.name > b.name) ? 1 : -1;
res *= order === 'desc' ? -1 : 1;
return res
})
case 'random':
return shuffle(proxies);
default:
throw new Error("Unknown sort option: " + order);
}
}
}
}
// sort by keywords
function KeywordSortOperator(...keywords) {
return {
name: "Keyword Sort Operator",
func: proxies => proxies.sort((a, b) => {
const oA = getKeywordOrder(keywords, a.name);
const oB = getKeywordOrder(keywords, b.name);
if (oA && !oB) return -1;
if (oB && !oA) return 1;
if (oA && oB) return oA < oB ? -1 : 1;
if ((!oA && !oB) || (oA && oB && oA === oB)) return a.name < b.name ? -1 : 1; // fallback to normal sort
})
}
}
function getKeywordOrder(keywords, str) {
let order = null;
for (let i = 0; i < keywords.length; i++) {
if (str.indexOf(keywords[i]) !== -1) {
order = i + 1; // plus 1 is important! 0 will be treated as false!!!
break;
}
}
return order;
}
// rename by keywords
// keywords: [{old: "old", now: "now"}]
function KeywordRenameOperator(...keywords) {
return {
name: "Keyword Rename Operator",
func: proxies => {
return proxies.map(proxy => {
for (const {old, now} of keywords) {
proxy.name = proxy.name.replace(old, now);
}
return proxy;
})
}
}
}
// rename by regex
// keywords: [{expr: "string format regex", now: "now"}]
function RegexRenameOperator(...regex) {
return {
name: "Regex Rename Operator",
func: proxies => {
return proxies.map(proxy => {
for (const {expr, now} of regex) {
proxy.name = proxy.name.replace(new RegExp(expr, "g"), now);
}
return proxy;
})
}
}
}
// delete keywords operator
// keywords: ['a', 'b', 'c']
function KeywordDeleteOperator(...keywords) {
const keywords_ = keywords.map(k => {
return {
old: k,
now: ""
}
})
return {
name: "Keyword Delete Operator",
func: KeywordRenameOperator(keywords_).func
}
}
// delete regex operator
// regex: ['a', 'b', 'c']
function RegexDeleteOperator(...regex) {
const regex_ = regex.map(r => {
return {
expr: r,
now: ""
}
});
return {
name: "Regex Delete Operator",
func: RegexRenameOperator(regex_).func
}
}
// use base64 encoded script to rename
/** Example script
function func(proxies) {
// do something
return proxies;
}
WARNING:
1. This function name should be `func`!
2. Always declare variable before using it!
*/
function ScriptOperator(script) {
return {
name: "Script Operator",
func: (proxies) => {
;(function () {
// interface to get internal operators
const $get = (name, args) => {
const operator = AVAILABLE_OPERATORS[name];
if (operator) {
return operator(args).func;
} else {
throw new Error(`No operator named ${name} is found!`);
}
};
eval(script);
return func(proxies);
})();
}
}
}
/**************************** Filters ***************************************/
// filter by keywords
function KeywordFilter(...keywords) {
return {
name: "Keyword Filter",
func: (proxies) => {
return proxies.map(proxy => keywords.some(k => proxy.name.indexOf(k) !== -1));
}
}
}
function DiscardKeywordFilter(...keywords) {
return {
name: "Discard Keyword Filter",
func: proxies => {
const filter = KeywordFilter(keywords).func;
return NOT(filter(proxies));
}
}
}
// filter useless proxies
function UselessFilter() {
const KEYWORDS = ["流量", "时间", "应急", "过期", "Bandwidth", "expire"];
return {
name: "Useless Filter",
func: DiscardKeywordFilter(KEYWORDS).func
}
}
// filter by regions
function RegionFilter(...regions) {
const REGION_MAP = {
"HK": "🇭🇰",
"TW": "🇹🇼",
"US": "🇺🇸",
"SG": "🇸🇬",
"JP": "🇯🇵",
"UK": "🇬🇧",
"KR": "🇰🇷"
};
return {
name: "Region Filter",
func: (proxies) => {
// this would be high memory usage
return proxies.map(proxy => {
const flag = getFlag(proxy.name);
return regions.some(r => REGION_MAP[r] === flag);
})
}
}
}
// filter by regex
function RegexFilter(...regex) {
return {
name: "Regex Filter",
func: (proxies) => {
return proxies.map(proxy => regex.some(r => r.test(proxy.name)));
}
}
}
function DiscardRegexFilter(...regex) {
return {
name: "Discard Regex Filter",
func: proxies => {
const filter = RegexFilter(regex).func;
return NOT(filter(proxies));
}
}
}
// filter by proxy types
function TypeFilter(...types) {
return {
name: "Type Filter",
func: (proxies) => {
return proxies.map(proxy => types.some(t => proxy.type === t));
}
}
}
// use base64 encoded script to filter proxies
/** Script Example
function func(proxies) {
const selected = FULL(proxies.length, true);
// do something
return selected;
}
WARNING:
1. This function name should be `func`!
2. Always declare variables before using them!
*/
function ScriptFilter(script) {
return {
name: "Script Filter",
func: (proxies) => {
!(function () {
// interface to get internal filters
const $get = (name, args) => {
const filter = AVAILABLE_FILTERS[name];
if (filter) {
return filter(args).func;
} else {
throw new Error(`No filter named ${name} is found!`);
}
};
eval(script);
return func(proxies);
})();
}
}
}
/******************************** Utility Functions *********************************************/
// get proxy flag according to its name
function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
const flags = {
"🏳️‍🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"],
"🇦🇨": ["AC"],
"🇦🇹": ["奥地利", "维也纳"],
"🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"],
"🇧🇪": ["BE", "比利时"],
"🇧🇬": ["保加利亚", "Bulgaria"],
"🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"],
"🇨🇦": ["Canada", "Waterloo", "加拿大", "蒙特利尔", "温哥华", "楓葉", "枫叶", "滑铁卢", "多伦多"],
"🇨🇭": ["瑞士", "苏黎世", "Switzerland"],
"🇩🇪": ["DE", "German", "GERMAN", "德国", "德國", "法兰克福"],
"🇩🇰": ["丹麦"],
"🇪🇸": ["ES", "西班牙", "Spain"],
"🇪🇺": ["EU", "欧盟", "欧罗巴"],
"🇫🇮": ["Finland", "芬兰", "赫尔辛基"],
"🇫🇷": ["FR", "France", "法国", "法國", "巴黎"],
"🇬🇧": ["UK", "GB", "England", "United Kingdom", "英国", "伦敦", "英"],
"🇲🇴": ["MO", "Macao", "澳门", "CTM"],
"🇭🇺": ["匈牙利", "Hungary"],
"🇭🇰": ["HK", "Hongkong", "Hong Kong", "香港", "深港", "沪港", "呼港", "HKT", "HKBN", "HGC", "WTT", "CMI", "穗港", "京港", "港"],
"🇮🇩": ["Indonesia", "印尼", "印度尼西亚", "雅加达"],
"🇮🇪": ["Ireland", "爱尔兰", "都柏林"],
"🇮🇳": ["India", "印度", "孟买", "Mumbai"],
"🇰🇵": ["KP", "朝鲜"],
"🇰🇷": ["KR", "Korea", "KOR", "韩国", "首尔", "韩", "韓"],
"🇱🇻": ["Latvia", "Latvija", "拉脱维亚"],
"🇲🇽️": ["MEX", "MX", "墨西哥"],
"🇲🇾": ["MY", "Malaysia", "马来西亚", "吉隆坡"],
"🇳🇱": ["NL", "Netherlands", "荷兰", "荷蘭", "尼德蘭", "阿姆斯特丹"],
"🇵🇭": ["PH", "Philippines", "菲律宾"],
"🇷🇴": ["RO", "罗马尼亚"],
"🇷🇺": ["RU", "Russia", "俄罗斯", "俄羅斯", "伯力", "莫斯科", "圣彼得堡", "西伯利亚", "新西伯利亚", "京俄", "杭俄"],
"🇸🇦": ["沙特", "迪拜"],
"🇸🇪": ["SE", "Sweden"],
"🇸🇬": ["SG", "Singapore", "新加坡", "狮城", "沪新", "京新", "泉新", "穗新", "深新", "杭新", "广新"],
"🇹🇭": ["TH", "Thailand", "泰国", "泰國", "曼谷"],
"🇹🇷": ["TR", "Turkey", "土耳其", "伊斯坦布尔"],
"🇹🇼": ["TW", "Taiwan", "台湾", "台北", "台中", "新北", "彰化", "CHT", "台", "HINET"],
"🇺🇸": ["US", "USA", "America", "United States", "美国", "美", "京美", "波特兰", "达拉斯", "俄勒冈", "凤凰城", "费利蒙", "硅谷", "矽谷", "拉斯维加斯", "洛杉矶", "圣何塞", "圣克拉拉", "西雅图", "芝加哥", "沪美", "哥伦布", "纽约"],
"🇻🇳": ["VN", "越南", "胡志明市"],
"🇮🇹": ["Italy", "IT", "Nachash", "意大利", "米兰", "義大利"],
"🇿🇦": ["South Africa", "南非"],
"🇦🇪": ["United Arab Emirates", "阿联酋"],
"🇯🇵": ["JP", "Japan", "日", "日本", "东京", "大阪", "埼玉", "沪日", "穗日", "川日", "中日", "泉日", "杭日", "深日", "辽日", "广日"],
"🇦🇷": ["AR", "阿根廷"],
"🇳🇴": ["Norway", "挪威", "NO"],
"🇨🇳": ["CN", "China", "回国", "中国", "江苏", "北京", "上海", "广州", "深圳", "杭州", "徐州", "青岛", "宁波", "镇江", "back"]
};
for (let k of Object.keys(flags)) {
if (flags[k].some((item => name.indexOf(item) !== -1))) {
return k;
}
}
// no flag found
const oldFlag = (name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/) || [])[0];
return oldFlag || "🏴‍☠️";
}
// remove flag
function removeFlag(str) {
return str.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, "").trim();
}
// clone an object
function clone(obj) {
return JSON.parse(JSON.stringify(obj))
}
// shuffle array
function shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// some logical functions for proxy filters
function AND(...args) {
return args.reduce((a, b) => a.map((c, i) => b[i] && c));
}
function OR(...args) {
return args.reduce((a, b) => a.map((c, i) => b[i] || c))
}
function NOT(array) {
return array.map(c => !c);
}
function FULL(length, bool) {
return [...Array(length).keys()].map(() => bool);
}
// UUID
// source: https://stackoverflow.com/questions/105034/how-to-create-guid-uuid
function UUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// get platform form UA
function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = "";
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
break;
}
}
if (UA.indexOf("Quantumult%20X") !== -1) {
return "QX";
} else if (UA.indexOf("Surge") !== -1) {
return "Surge";
} else if (UA.indexOf("Decar") !== -1) {
return "Loon";
} else {
// browser
return "Raw";
}
}
/*********************************** OpenAPI *************************************/
// OpenAPI
// prettier-ignore
function ENV() {
const isQX = typeof $task != "undefined";
const isLoon = typeof $loon != "undefined";
const isSurge = typeof $httpClient != "undefined" && !this.isLoon;
const isJSBox = typeof require == "function" && typeof $jsbox != "undefined";
const isNode = typeof require == "function" && !isJSBox;
const isRequest = typeof $request !== "undefined";
return {isQX, isLoon, isSurge, isNode, isJSBox, isRequest};
}
function HTTP(baseURL, defaultOptions = {}) {
const {isQX, isLoon, isSurge} = ENV();
const methods = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"];
function send(method, options) {
options = typeof options === "string" ? {url: options} : options;
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 {
worker = new Promise((resolve, reject) => {
const request = isSurge || isLoon ? $httpClient : require("request");
request[method.toLowerCase()](options, (err, response, body) => {
if (err) reject(err);
else
resolve({
statusCode: response.status || response.statusCode,
headers: response.headers,
body,
});
});
});
}
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} = 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);
});
};
}
// persistance
// 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);
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),
{flag: "w"},
(err) => console.log(err)
);
}
}
write(data, key) {
this.log(`SET ${key}`);
if (key.indexOf("#") !== -1) {
key = key.substr(1);
if (isSurge & isLoon) {
$persistentStore.write(data, key);
}
if (isQX) {
$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) {
$persistentStore.write(null, key);
}
if (isQX) {
$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"];
const content_ =
content +
(openURL ? `\n点击跳转: ${openURL}` : "") +
(mediaURL ? `\n多媒体: ${mediaURL}` : "");
if (isQX) $notify(title, subtitle, content, options);
if (isSurge) $notification.post(title, subtitle, content_);
if (isLoon) $notification.post(title, subtitle, content, openURL);
if (isNode) {
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(msg);
}
info(msg) {
console.log(msg);
}
error(msg) {
console.log("ERROR: " + msg);
}
})(name, debug);
}
/*********************************** Mini Express *************************************/
function express(port = 3000) {
const {isNode} = ENV();
const DEFAULT_HEADERS = {
"Content-Type": "text/plain;charset=UTF-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"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, () => {
$.log(`Express started on port: ${port}`);
})
}
return app;
}
// route handlers
const handlers = [];
// http methods
const METHODS_NAMES = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS",
"HEAD'",
"ALL",
];
// dispatch url to route
const dispatch = (request, start = 0) => {
let {method, url, headers, body} = request;
if (/json/i.test(headers['Content-Type'])) {
body = JSON.parse(body);
}
method = method.toUpperCase();
const {path, query} = extractURL(url);
let handler = null;
let i;
for (i = start; i < handlers.length; i++) {
if (handlers[i].method === "ALL" || method === handlers[i].method) {
const {pattern} = handlers[i];
if (patternMatched(pattern, path)) {
handler = handlers[i];
break;
}
}
}
if (handler) {
$.notify(`DISPATCHING:`, `${method}, ${url}`, path);
// dispatch to next handler
const next = () => {
dispatch(method, url, i);
};
const req = {
method, url, path, query,
params: extractPathParams(handler.pattern, path),
headers, body
};
const res = Response();
handler.callback(req, res, next).catch(err => {
res.status(500).json({
status: "failed",
message: "Internal Server Error"
});
});
} 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;
return new (class {
status(code) {
statusCode = code;
return this;
}
send(body = "") {
const response = {
status: statusCode,
body,
headers,
};
if (isQX) {
$done(...response);
} else if (isLoon || isSurge) {
$done({
response,
});
}
}
end() {
this.send();
}
html(data) {
this.set("Content-Type", "text/html;charset=UTF-8");
this.send(data);
}
json(data) {
this.set("Content-Type", "application/json;charset=UTF-8");
this.send(JSON.stringify(data));
}
set(key, val) {
headers[key] = val;
return this;
}
})();
}
function patternMatched(pattern, path) {
if (pattern instanceof RegExp && pattern.test(path)) {
return true;
} else {
// root pattern, match all
if (pattern === "/") return true;
// normal string pattern
if (pattern.indexOf(":") === -1) {
const spath = path.split("/");
const spattern = pattern.split("/");
for (let i = 0; i < spattern.length; i++) {
if (spath[i] !== spattern[i]) {
return false;
}
}
return true;
}
// string pattern with path parameters
else if (extractPathParams(pattern, path)) {
return true;
}
}
return false;
}
function extractURL(url) {
// extract path
const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
const path = match[1] || "/";
// extract query string
const split = url.indexOf("?");
const query = {};
if (split !== -1) {
let hashes = url.slice(url.indexOf("?") + 1).split("&");
for (let i = 0; i < hashes.length; i++) {
hash = hashes[i].split("=");
query[hash[0]] = hash[1];
}
}
return {
path,
query,
};
}
function extractPathParams(pattern, path) {
if (pattern.indexOf(":") === -1) {
return null;
} else {
const params = {};
for (let i = 0, j = 0; i < pattern.length; i++, j++) {
if (pattern[i] === ":") {
let key = [];
let val = [];
while (pattern[++i] !== "/" && i < pattern.length) {
key.push(pattern[i]);
}
while (path[j] !== "/" && j < path.length) {
val.push(path[j++]);
}
params[key.join("")] = val.join("");
} else {
if (pattern[i] !== path[j]) {
return null;
}
}
}
return params;
}
}
}
/******************************** Base 64 *********************************************/
// Base64 Coding Library
// https://github.com/dankogai/js-base64#readme
// Under BSD License
function Base64Code() {
// constants
const b64chars
= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const b64tab = function (bin) {
const t = {};
let i = 0;
const l = bin.length;
for (; i < l; i++) t[bin.charAt(i)] = i;
return t;
}(b64chars);
const fromCharCode = String.fromCharCode;
// encoder stuff
const cb_utob = function (c) {
let cc;
if (c.length < 2) {
cc = c.charCodeAt(0);
return cc < 0x80 ? c
: cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6))
+ fromCharCode(0x80 | (cc & 0x3f)))
: (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f))
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
+ fromCharCode(0x80 | (cc & 0x3f)));
} else {
cc = 0x10000
+ (c.charCodeAt(0) - 0xD800) * 0x400
+ (c.charCodeAt(1) - 0xDC00);
return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07))
+ fromCharCode(0x80 | ((cc >>> 12) & 0x3f))
+ fromCharCode(0x80 | ((cc >>> 6) & 0x3f))
+ fromCharCode(0x80 | (cc & 0x3f)));
}
};
const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
const utob = function (u) {
return u.replace(re_utob, cb_utob);
};
const cb_encode = function (ccc) {
const padlen = [0, 2, 1][ccc.length % 3],
ord = ccc.charCodeAt(0) << 16
| ((ccc.length > 1 ? ccc.charCodeAt(1) : 0) << 8)
| ((ccc.length > 2 ? ccc.charCodeAt(2) : 0)),
chars = [
b64chars.charAt(ord >>> 18),
b64chars.charAt((ord >>> 12) & 63),
padlen >= 2 ? '=' : b64chars.charAt((ord >>> 6) & 63),
padlen >= 1 ? '=' : b64chars.charAt(ord & 63)
];
return chars.join('');
};
const btoa = function (b) {
return b.replace(/[\s\S]{1,3}/g, cb_encode);
};
this.encode = function (u) {
const isUint8Array = Object.prototype.toString.call(u) === '[object Uint8Array]';
return isUint8Array ? u.toString('base64')
: btoa(utob(String(u)));
}
const uriencode = function (u, urisafe) {
return !urisafe
? _encode(u)
: _encode(String(u)).replace(/[+\/]/g, function (m0) {
return m0 === '+' ? '-' : '_';
}).replace(/=/g, '');
};
const encodeURI = function (u) {
return uriencode(u, true)
};
// decoder stuff
const re_btou = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
const cb_btou = function (cccc) {
switch (cccc.length) {
case 4:
const cp = ((0x07 & cccc.charCodeAt(0)) << 18)
| ((0x3f & cccc.charCodeAt(1)) << 12)
| ((0x3f & cccc.charCodeAt(2)) << 6)
| (0x3f & cccc.charCodeAt(3)),
offset = cp - 0x10000;
return (fromCharCode((offset >>> 10) + 0xD800)
+ fromCharCode((offset & 0x3FF) + 0xDC00));
case 3:
return fromCharCode(
((0x0f & cccc.charCodeAt(0)) << 12)
| ((0x3f & cccc.charCodeAt(1)) << 6)
| (0x3f & cccc.charCodeAt(2))
);
default:
return fromCharCode(
((0x1f & cccc.charCodeAt(0)) << 6)
| (0x3f & cccc.charCodeAt(1))
);
}
};
const btou = function (b) {
return b.replace(re_btou, cb_btou);
};
const cb_decode = function (cccc) {
const len = cccc.length,
padlen = len % 4,
n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0)
| (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0)
| (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0)
| (len > 3 ? b64tab[cccc.charAt(3)] : 0),
chars = [
fromCharCode(n >>> 16),
fromCharCode((n >>> 8) & 0xff),
fromCharCode(n & 0xff)
];
chars.length -= [0, 0, 2, 1][padlen];
return chars.join('');
};
const _atob = function (a) {
return a.replace(/\S{1,4}/g, cb_decode);
};
const atob = function (a) {
return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g, ''));
};
const _decode = function (u) {
return btou(_atob(u))
};
this.decode = function (a) {
return _decode(
String(a).replace(/[-_]/g, function (m0) {
return m0 === '-' ? '+' : '/'
})
.replace(/[^A-Za-z0-9\+\/]/g, '')
).replace(/&gt;/g, ">").replace(/&lt;/g, "<");
};
this.safeEncode = function (a) {
return this.encode(a.replace(/\+/g, "-").replace(/\//g, "_"));
}
this.safeDecode = function (a) {
return this.decode(a.replace(/-/g, "+").replace(/_/g, "/"));
}
}
/*
YAML parser for Javascript
Author: Diogo Costa
This program is released under the MIT License
*/
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;
continue;
} 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]));
}
continue;
}
}
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;
}
}
})();