Sub-Store/backend/sub-store.js

4433 lines
159 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.

/**
* ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
* ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
* ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/
const $ = API("sub-store");
const Base64 = new Base64Code();
service();
/****************************************** Service **********************************************************/
function service() {
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 © 𝑷𝒆𝒏𝒈-𝒀𝑴
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`);
const $app = express();
// Constants
const SETTINGS_KEY = "settings";
const SUBS_KEY = "subs";
const COLLECTIONS_KEY = "collections";
const RULES_KEY = "rules";
const BUILT_IN_KEY = "builtin";
const ARTIFACTS_KEY = "artifacts";
const GIST_BACKUP_KEY = "Auto Generated Sub-Store Backup";
const GIST_BACKUP_FILE_NAME = "Sub-Store";
const ARTIFACT_REPOSITORY_KEY = "Sub-Store Artifacts Repository";
// Initialization
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY);
if (!$.read(RULES_KEY)) $.write({}, RULES_KEY);
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
$.write({
rules: getBuiltInRules(),
}, BUILT_IN_KEY);
// download
$app.get("/download/collection/:name", downloadCollection);
$app.get("/download/:name", downloadSubscription);
// subscription API
$app.route("/api/sub/:name")
.get(getSubscription)
.patch(updateSubscription)
.delete(deleteSubscription);
$app.route("/api/subs")
.get(getAllSubscriptions)
.post(createSubscription);
$app.get("/api/sub/statistics/:name");
// collection API
$app.route("/api/collection/:name")
.get(getCollection)
.patch(updateCollection)
.delete(deleteCollection);
$app.route("/api/collections")
.get(getAllCollections)
.post(createCollection);
// rules API
$app.get("/download/rule/:name", downloadRule);
$app.route("/api/rules")
.post(createRule)
.get(getAllRules);
$app.route("/api/rule/:name")
.patch(updateRule)
.delete(deleteRule)
.get(getRule);
// Storage management
$app.route("/api/storage")
.get((req, res) => {
res.json($.read("#sub-store"));
})
.post((req, res) => {
const data = req.body;
$.write(JSON.stringify(data), "#sub-store");
res.end();
});
// Settings
$app.route("/api/settings")
.get(getSettings)
.patch(updateSettings);
// Artifacts
$app.route("/api/artifacts")
.get(getAllArtifacts)
.post(createArtifact);
$app.route("/api/artifact/:name")
.get(getArtifact)
.patch(updateArtifact)
.delete(deleteArtifact);
// utils
$app.get("/api/utils/IP_API/:server", IP_API); // IP-API reverse proxy
$app.post("/api/utils/refresh", refreshCache); // force refresh resource
$app.get("/api/utils/env", getEnv); // get runtime environment
$app.get("/api/utils/backup", gistBackup); // gist backup actions
// Redirect sub.store to vercel webpage
$app.get("/", async (req, res) => {
// 302 redirect
res.set("location", "https://sub-store.vercel.app/").status(302).end();
});
// handle preflight request for QX
if (ENV().isQX) {
$app.options("/", async (req, res) => {
res.status(200).end();
});
}
$app.all("/", (req, res) => {
res.send("Hello from sub-store, made with ❤️ by Peng-YM");
});
$app.start();
// subscriptions API
async function downloadSubscription(req, res) {
const {name} = req.params;
const {cache} = req.query;
const {raw} = req.query || "false";
const platform = req.query.target || getPlatformFromHeaders(req.headers) || "JSON";
const useCache = typeof cache === 'undefined' ? (platform === 'JSON' || platform === 'URI') : cache;
$.info(`正在下载订阅:${name}`);
const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name];
if (sub) {
try {
const output = await produceArtifact({
type: 'subscription',
item: sub,
platform,
useCache,
noProcessor: raw
});
if (platform === 'JSON') {
res.set("Content-Type", "application/json;charset=utf-8").send(output);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${err}`
);
res.status(500).json({
status: "failed",
message: err,
});
}
} else {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
`❌ 未找到订阅:${name}`,
);
res.status(404).json({
status: "failed",
});
}
}
function createSubscription(req, res) {
const sub = req.body;
const allSubs = $.read(SUBS_KEY);
$.info(`正在创建订阅: ${sub.name}`);
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} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
function getSubscription(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}!`,
});
}
}
function updateSubscription(req, res) {
const {name} = req.params;
let sub = req.body;
const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) {
const newSub = {
...allSubs[name],
...sub,
};
$.info(`正在更新订阅: ${name}`);
// 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}不存在,无法更新!`,
});
}
}
function deleteSubscription(req, res) {
const {name} = req.params;
$.info(`删除订阅:${name}...`);
// 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",
});
}
function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY);
res.json({
status: "success",
data: allSubs,
});
}
// collection API
async function downloadCollection(req, res) {
const {name} = req.params;
const {cache} = req.query || "false";
const {raw} = req.query || "false";
const platform = req.query.target || getPlatformFromHeaders(req.headers) || "JSON";
const useCache = typeof cache === 'undefined' ? (platform === 'JSON' || platform === 'URI') : cache;
const allCollections = $.read(COLLECTIONS_KEY);
const collection = allCollections[name];
$.info(`正在下载组合订阅:${name}`);
if (collection) {
try {
const output = await produceArtifact({
type: "collection",
item: collection,
platform,
useCache,
noProcessor: raw
});
if (platform === 'JSON') {
res.set("Content-Type", "application/json;charset=utf-8").send(output);
} else {
res.send(output);
}
} catch (err) {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 下载组合订阅错误:${name}`,
`🤔 原因:${err}`
);
res.status(500).json({
status: "failed",
message: err,
});
}
} else {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
res.status(404).json({
status: "failed",
});
}
}
function createCollection(req, res) {
const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`);
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} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
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}!`,
});
}
}
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,
};
$.info(`正在更新组合订阅:${name}...`);
// 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}不存在,无法更新!`,
});
}
}
function deleteCollection(req, res) {
const {name} = req.params;
$.info(`正在删除组合订阅:${name}`);
let allCol = $.read(COLLECTIONS_KEY);
delete allCol[name];
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: "success",
});
}
function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
res.json({
status: "success",
data: allCols,
});
}
// rules API
async function downloadRule(req, res) {
const {name} = req.params;
const {builtin} = req.query;
const platform = req.query.target || getPlatformFromHeaders(req.headers) || "Surge";
$.info(`正在下载${builtin ? "内置" : ""}分流订阅:${name}...`);
let rule;
if (builtin) {
rule = $.read(BUILT_IN_KEY)['rules'][name];
}
if (rule) {
const output = await produceArtifact({type: "rule", item: rule, platform})
res.send(output);
} else {
// rule not found
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载分流订阅失败`,
`❌ 未找到分流订阅:${name}`,
);
res.status(404).json({
status: "failed",
});
}
}
function createRule(req, res) {
}
function deleteRule(req, res) {
}
function updateRule(req, res) {
}
function getAllRules(req, res) {
}
function getRule(req, res) {
}
// settings API
function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY);
res.json(settings);
}
function updateSettings(req, res) {
const data = req.body;
const settings = $.read(SETTINGS_KEY);
$.write({
...settings,
...data
}, SETTINGS_KEY);
res.json({
status: "success"
});
}
// artifact API
async function getArtifact(req, res) {
const name = req.params.name;
const action = req.query.action;
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = allArtifacts[name];
if (artifact) {
if (action) {
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({
type: artifact.type,
item,
platform: artifact.platform
})
if (action === 'preview') {
res.send(output);
} else if (action === 'sync') {
$.info(`正在上传配置:${artifact.name}\n>>>`);
console.log(JSON.stringify(artifact, null, 2));
try {
const resp = await syncArtifact({
filename: artifact.name,
content: output
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[artifact.name].raw_url.replace(/\/raw\/[^\/]*\/(.*)/, "/raw/$1");
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: "success"
});
} catch (err) {
res.status(500).json({
status: "failed",
message: err
});
}
}
} else {
res.json({
status: "success",
data: artifact
});
}
} else {
res.status(404).json({
status: "failed",
message: "未找到对应的配置!",
});
}
}
function createArtifact(req, res) {
const artifact = req.body;
$.info(`正在创建远程配置:${artifact.name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
if (allArtifacts[artifact.name]) {
res.status(500).json({
status: "failed",
message: `远程配置${artifact.name}已存在!`,
});
} else {
if (/^[\w-_.]*$/.test(artifact.name)) {
allArtifacts[artifact.name] = artifact;
$.write(allArtifacts, ARTIFACTS_KEY);
res.status(201).json({
status: "success",
data: artifact
});
} else {
res.status(500).json({
status: "failed",
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
}
function updateArtifact(req, res) {
const allArtifacts = $.read(SETTINGS_KEY);
const oldName = req.params.name;
const artifact = allArtifacts[oldName];
if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`);
const newArtifact = req.body;
if (typeof newArtifact.name !== 'undefined' && !/^[\w-_.]*$/.test(newArtifact.name)) {
res.status(500).json({
status: "failed",
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
} else {
const merged = {
...artifact,
...newArtifact
};
allArtifacts[merged.name] = merged;
if (merged.name !== oldName) delete allArtifacts[oldName];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: "success",
data: merged
});
}
} else {
res.status(404).json({
status: "failed",
message: "未找到对应的远程配置!",
});
}
}
async function deleteArtifact(req, res) {
const name = req.params.name;
$.info(`正在删除Artifact${name}`);
try {
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = allArtifacts[name];
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) {
// delete gist
await syncArtifact({
filename: name,
content: ""
});
}
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: "success"
});
} catch (err) {
res.status(500).json({
status: "failed",
message: `无法删除远程配置:${name}, 原因:${err}`
});
}
}
function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({
status: "success",
data: allArtifacts
});
}
async function syncArtifact({filename, content}) {
const {gistToken} = $.read(SETTINGS_KEY);
if (!gistToken) {
return Promise.reject("未设置Gist Token");
}
const manager = new Gist({
token: gistToken,
key: ARTIFACT_REPOSITORY_KEY
});
return manager.upload({filename, content});
}
// util API
async function IP_API(req, res) {
const server = decodeURIComponent(req.params.server);
const result = await $.http
.get(`http://ip-api.com/json/${server}?lang=zh-CN`)
.then((resp) => JSON.parse(resp.body));
res.json(result);
}
async function refreshCache(req, res) {
const {url} = req.body;
$.info(`Refreshing cache for URL: ${url}`);
try {
const raw = await getResource(url, false);
$.write(raw, `#${Base64.safeEncode(url)}`);
res.json({
status: "success",
});
} catch (err) {
res.status(500).json({
status: "failed",
message: `无法刷新资源 ${url} ${err}`
});
}
}
function getEnv(req, res) {
const {isNode, isQX, isLoon, isSurge} = ENV();
let backend = "Node";
if (isNode) backend = "Node";
if (isQX) backend = "QX";
if (isLoon) backend = "Loon";
if (isSurge) backend = "Surge";
res.json({
backend
});
}
async function gistBackup(req, res) {
const {action} = req.query;
// read token
const {gistToken} = $.read(SETTINGS_KEY);
if (!gistToken) {
res.status(500).json({
status: "failed",
message: "未找到Gist备份Token!"
});
} else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY
});
try {
let content;
switch (action) {
case "upload":
// update syncTime.
const settings = $.read(SETTINGS_KEY);
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read("#sub-store");
$.info(`上传备份中...`);
await gist.upload({filename: GIST_BACKUP_FILE_NAME, content});
break;
case "download":
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
// restore settings
$.write(content, "#sub-store");
break;
}
res.json({
status: "success",
});
} catch (err) {
const msg = `${action === "upload" ? "上传" : "下载"}备份失败!${err}`;
$.error(msg);
res.status(500).json({
status: "failed",
message: msg
});
}
}
}
// get target platform from user agent
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 || UA.indexOf("Loon") !== -1) {
return "Loon";
} else {
return null;
}
}
// get resource, with cache ability to speedup response time
async function getResource(url, useCache = true) {
// use QX agent to get flow headers
const $http = HTTP({
headers: {
"User-Agent": "Quantumult%20X",
}
});
const key = "#" + Base64.safeEncode(url);
const resource = $.read(key);
const timeKey = `#TIME-${Base64.safeEncode(url)}`;
const ONE_HOUR = 60 * 60 * 1000;
const outdated = new Date().getTime() - $.read(timeKey) > ONE_HOUR;
if (useCache && resource && !outdated) {
$.log(`Use cached for resource: ${url}`);
return resource;
}
let body = "";
try {
const resp = await $http.get(url);
body = resp.body;
} catch (err) {
throw new Error(err);
} finally {
$.write(body, key);
$.write(new Date().getTime(), timeKey);
}
if (body.replace(/\s/g, "").length === 0) {
throw new Error("订阅内容为空!");
}
return body;
}
async function produceArtifact({type, item, platform, useCache, noProcessor} = {
platform: "JSON",
useCache: false,
noProcessor: false
}) {
if (type === 'subscription') {
const sub = item;
const raw = await getResource(sub.url, useCache);
// parse proxies
let proxies = ProxyUtils.parse(raw);
if (!noProcessor) {
// apply processors
proxies = await ProxyUtils.process(proxies, sub.process || []);
}
// produce
return ProxyUtils.produce(proxies, platform);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const collection = item;
const subs = collection['subscriptions'];
let proxies = [];
for (let i = 0; i < subs.length; i++) {
const sub = allSubs[subs[i]];
$.info(`正在处理子订阅:${sub.name},进度--${100 * ((i + 1) / subs.length).toFixed(1)}% `);
try {
const raw = await getResource(sub.url, useCache);
// parse proxies
let currentProxies = ProxyUtils.parse(raw)
if (!noProcessor) {
// apply processors
currentProxies = await ProxyUtils.process(currentProxies, sub.process || []);
}
// merge
proxies = proxies.concat(currentProxies);
} catch (err) {
$.error(`处理组合订阅中的子订阅: ${sub.name}时出现错误:${err}! 该订阅已被跳过。`);
}
}
if (!noProcessor) {
// apply own processors
proxies = await ProxyUtils.process(proxies, collection.process || []);
}
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点!`);
}
return ProxyUtils.produce(proxies, platform);
} else if (type === 'rule') {
const rule = item;
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
$.info(`正在处理URL${url},进度--${100 * ((i + 1) / rule.urls.length).toFixed(1)}% `);
try {
const {body} = await $.http.get(url);
const currentRules = RuleUtils.parse(body);
rules = rules.concat(currentRules);
} catch (err) {
$.error(`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`);
}
}
// remove duplicates
rules = await RuleUtils.process(rules, [{type: "Remove Duplicate Filter"}]);
// produce output
return RuleUtils.produce(rules, platform);
}
}
}
/****************************************** Proxy Utils **********************************************************/
var ProxyUtils = (function () {
const PROXY_PREPROCESSORS = (function () {
function HTML() {
const name = "HTML";
const test = raw => /^<!DOCTYPE html>/.test(raw);
// simply discard HTML
const parse = _ => "";
return {name, test, parse};
}
function Base64Encoded() {
const name = "Base64 Pre-processor";
const keys = ["dm1lc3M", "c3NyOi8v", "dHJvamFu", "c3M6Ly", "c3NkOi8v",
"c2hhZG93", "aHR0c"
];
const test = function (raw) {
return keys.some(k => raw.indexOf(k) !== -1);
}
const parse = function (raw) {
raw = Base64.safeDecode(raw);
return raw;
}
return {name, test, parse};
}
function Clash() {
const name = "Clash Pre-processor";
const test = function (raw) {
return /proxies/.test(raw);
}
const parse = function (raw) {
// Clash YAML format
// codes are modified from @KOP-XIAO
// https://github.com/KOP-XIAO/QuantumultX
if (raw.indexOf("{") !== -1) {
raw = raw
.replace(/ - /g, " - ")
.replace(/:(?!\s)/g, ": ")
.replace(/\,\"/g, ', "')
.replace(/: {/g, ": {, ")
.replace(/, (\"?host|path|tls|mux|skip\"?)/g, ", $1")
.replace(/{name: /g, '{name: "')
.replace(/, server:/g, '", server:')
.replace(/{|}/g, "")
.replace(/,/g, "\n ");
}
raw = raw.replace(/ -\n.*name/g, " - name")
.replace(/\$|\`/g, "")
.split("proxy-providers:")[0]
.split("proxy-groups:")[0]
.replace(/\"([\w-]+)\"\s*:/g, "$1:")
raw = raw.indexOf("proxies:") === -1 ? "proxies:\n" + raw : "proxies:" + raw.split("proxies:")[1]
const proxies = YAML.eval(raw).proxies;
return proxies.map(p => JSON.stringify(p)).join("\n");
}
return {name, test, parse};
}
function SSD() {
const name = "SSD Pre-processor";
const test = function (raw) {
return raw.indexOf("ssd://") === 0;
};
const parse = function (raw) {
// preprocessing for SSD subscription format
const output = [];
let ssdinfo = JSON.parse(Base64.safeDecode(raw.split("ssd://")[1]));
// options (traffic_used, traffic_total, expiry, url)
const traffic_used = ssdinfo.traffic_used; // GB
const traffic_total = ssdinfo.traffic_total; // GB, -1 means unlimited
const expiry = ssdinfo.expiry; // YYYY-MM-DD HH:mm:ss
// default setting
let name = ssdinfo.airport; // name of the airport
let port = ssdinfo.port;
let method = ssdinfo.encryption;
let password = ssdinfo.password;
// servers config
let servers = ssdinfo.servers;
for (let i = 0; i < servers.length; i++) {
let server = servers[i];
method = server.encryption ? server.encryption : method;
password = server.password ? server.password : password;
let userinfo = Base64.safeEncode(method + ":" + password);
let hostname = server.server;
port = server.port ? server.port : port;
let tag = server.remarks ? server.remarks : i;
let plugin = server.plugin_options
? "/?plugin=" +
encodeURIComponent(server.plugin + ";" + server.plugin_options)
: "";
output[i] =
"ss://" + userinfo + "@" + hostname + ":" + port + plugin + "#" + tag;
}
return output.join("\n");
};
return {name, test, parse};
}
return [
HTML(), Base64Encoded(), Clash(), SSD()
];
})();
const PROXY_PARSERS = (function () {
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
function URI_SS() {
const name = "URI SS Parser";
const test = (line) => {
return /^ss:\/\//.test(line);
};
const parse = (line) => {
const supported = {};
// parse url
let content = line.split("ss://")[1];
const proxy = {
name: decodeURIComponent(line.split("#")[1]),
type: "ss",
supported,
};
content = content.split("#")[0]; // strip proxy name
// handle IPV4 and IPV6
const serverAndPort = content.match(/@([^\/]*)(\/|$)/)[1];
const portIdx = serverAndPort.lastIndexOf(":");
proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = serverAndPort.substring(portIdx + 1);
const userInfo = Base64.safeDecode(content.split("@")[0]).split(":");
proxy.cipher = userInfo[0];
proxy.password = userInfo[1];
// handle obfs
const idx = content.indexOf("?plugin=");
if (idx !== -1) {
const pluginInfo = (
"plugin=" +
decodeURIComponent(content.split("?plugin=")[1].split("&")[0])
).split(";");
const params = {};
for (const item of pluginInfo) {
const [key, val] = item.split("=");
if (key) params[key] = val || true; // some options like "tls" will not have value
}
switch (params.plugin) {
case "obfs-local":
case "simple-obfs":
proxy.plugin = "obfs";
proxy["plugin-opts"] = {
mode: params.obfs,
host: params["obfs-host"],
};
break;
case "v2ray-plugin":
proxy.supported = {
...supported,
Loon: false,
Surge: false,
};
proxy.obfs = "v2ray-plugin";
proxy["plugin-opts"] = {
mode: "websocket",
host: params["obfs-host"],
path: params.path || "",
tls: params.tls || false,
};
break;
default:
throw new Error(`Unsupported plugin option: ${params.plugin}`);
}
}
return proxy;
};
return {name, test, parse};
}
// Parse URI SSR format, such as ssr://xxx
function URI_SSR() {
const name = "URI SSR Parser";
const test = (line) => {
return /^ssr:\/\//.test(line);
};
const supported = {
Surge: false,
};
const parse = (line) => {
line = Base64.safeDecode(line.split("ssr://")[1]);
// handle IPV6 & IPV4 format
let splitIdx = line.indexOf(":origin");
if (splitIdx === -1) {
splitIdx = line.indexOf(":auth_");
}
const serverAndPort = line.substring(0, splitIdx);
const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
const port = serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1);
let params = line
.substring(splitIdx + 1)
.split("/?")[0]
.split(":");
let proxy = {
type: "ssr",
server,
port,
protocol: params[0],
cipher: params[1],
obfs: params[2],
password: Base64.safeDecode(params[3]),
supported,
};
// get other params
const other_params = {};
line = line.split("/?")[1].split("&");
if (line.length > 1) {
for (const item of line) {
const [key, val] = item.split("=");
other_params[key] = val.trim();
}
}
proxy = {
...proxy,
name: other_params.remarks ? Base64.safeDecode(other_params.remarks) : proxy.server,
"protocol-param":
Base64.safeDecode(other_params.protoparam || "").replace(/\s/g, ""),
"obfs-param":
Base64.safeDecode(other_params.obfsparam || "").replace(/\s/g, ""),
};
return proxy;
};
return {name, test, parse};
}
// V2rayN URI VMess format
// reference: https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
// Quantumult VMess format
function URI_VMess() {
const name = "URI VMess Parser";
const test = (line) => {
return /^vmess:\/\//.test(line);
};
const parse = (line) => {
const supported = {};
line = line.split("vmess://")[1];
const content = Base64.safeDecode(line);
if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format
const partitions = content.split(",").map((p) => p.trim());
// get keyword params
const params = {};
for (const part of partitions) {
if (part.indexOf("=") !== -1) {
const [key, val] = part.split("=");
params[key.trim()] = val.trim();
}
}
const proxy = {
name: partitions[0].split("=")[0].trim(),
type: "vmess",
server: partitions[1],
port: partitions[2],
cipher: partitions[3],
uuid: partitions[4].match(/^"(.*)"$/)[1],
tls: params.obfs === "over-tls" || params.obfs === "wss",
};
if (typeof params['udp-relay'] !== "undefined") proxy.udp = JSON.parse(params["udp-relay"]);
if (typeof params['fast-open'] !== "undefined") proxy.udp = JSON.parse(params["fast-open"]);
// handle ws headers
if (params.obfs === "ws" || params.obfs === "wss") {
proxy.network = "ws";
proxy["ws-path"] = (params["obfs-path"] || '"/"').match(/^"(.*)"$/)[1];
let obfs_host = params["obfs-header"];
if (obfs_host && obfs_host.indexOf("Host") !== -1) {
obfs_host = obfs_host.match(/Host:\s*([a-zA-Z0-9-.]*)/)[1];
}
proxy["ws-headers"] = {
Host: obfs_host || proxy.server, // if no host provided, use the same as server
};
}
// handle scert
if (proxy.tls && params['"tls-verification"'] === "false") {
proxy['skip-cert-verify'] = true;
}
// handle sni
if (proxy.tls && params["obfs-host"]) {
proxy.sni = params["obfs-host"];
}
return proxy;
} else {
// V2rayN URI format
const params = JSON.parse(content);
const proxy = {
name: params.ps,
type: "vmess",
server: params.add,
port: params.port,
cipher: "auto", // V2rayN has no default cipher! use aes-128-gcm as default.
uuid: params.id,
alterId: params.aid || 0,
tls: params.tls === "tls" || params.tls === true,
supported,
};
// handle obfs
if (params.net === "ws") {
proxy.network = "ws";
proxy["ws-path"] = params.path;
proxy["ws-headers"] = {
Host: params.host || params.add,
};
if (proxy.tls && params.host) {
proxy.sni = params.host;
}
}
// handle scert
if (params.verify_cert === false) {
proxy['skip-cert-verify'] = true;
}
return proxy;
}
};
return {name, test, parse};
}
// Trojan URI format
function URI_Trojan() {
const name = "URI Trojan Parser";
const test = (line) => {
return /^trojan:\/\//.test(line);
};
const parse = (line) => {
const supported = {};
line = line.split("trojan://")[1];
const [server, port] = line.split("@")[1].split("?")[0].split(":");
const name = decodeURIComponent(line.split("#")[1].trim());
return {
name: name || `[Trojan] ${server}`, // trojan uri may have no server tag!
type: "trojan",
server,
port,
password: line.split("@")[0],
supported,
};
};
return {name, test, parse};
}
function Clash_All() {
const name = "Clash Parser";
const test = (line) => {
try {
JSON.parse(line);
} catch (e) {
return false;
}
return true;
};
const parse = (line) => JSON.parse(line);
return {name, test, parse};
}
function QX_SS() {
const name = "QX SS Parser";
const test = (line) => {
return (
/^shadowsocks\s*=/.test(line.split(",")[0].trim()) &&
line.indexOf("ssr-protocol") === -1
);
};
const parse = (line) => {
const supported = {};
const params = getQXParams(line);
const proxy = {
name: params.tag,
type: "ss",
server: params.server,
port: params.port,
cipher: params.method,
password: params.password,
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
supported,
};
// handle obfs options
if (params.obfs) {
proxy["plugin-opts"] = {
host: params["obfs-host"] || proxy.server,
};
switch (params.obfs) {
case "http":
case "tls":
proxy.plugin = "obfs";
proxy["plugin-opts"].mode = params.obfs;
break;
case "ws":
case "wss":
proxy["plugin-opts"] = {
...proxy["plugin-opts"],
mode: "websocket",
path: params["obfs-uri"] || "/",
tls: params.obfs === "wss",
};
if (proxy["plugin-opts"].tls && typeof params['tls-verification'] !== "undefined") {
proxy["plugin-opts"]['skip-cert-verify'] = params['tls-verification'];
}
proxy.plugin = "v2ray-plugin";
// Surge and Loon lack support for v2ray-plugin obfs
proxy.supported.Surge = false;
proxy.supported.Loon = false;
break;
}
}
return proxy;
};
return {name, test, parse};
}
function QX_SSR() {
const name = "QX SSR Parser";
const test = (line) => {
return (
/^shadowsocks\s*=/.test(line.split(",")[0].trim()) &&
line.indexOf("ssr-protocol") !== -1
);
};
const parse = (line) => {
const supported = {
Surge: false,
};
const params = getQXParams(line);
const proxy = {
name: params.tag,
type: "ssr",
server: params.server,
port: params.port,
cipher: params.method,
password: params.password,
protocol: params["ssr-protocol"],
obfs: "plain", // default obfs
"protocol-param": params["ssr-protocol-param"],
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
supported,
};
// handle obfs options
if (params.obfs) {
proxy.obfs = params.obfs;
proxy["obfs-param"] = params["obfs-host"];
}
return proxy;
};
return {name, test, parse};
}
function QX_VMess() {
const name = "QX VMess Parser";
const test = (line) => {
return /^vmess\s*=/.test(line.split(",")[0].trim());
};
const parse = (line) => {
const params = getQXParams(line);
const proxy = {
type: "vmess",
name: params.tag,
server: params.server,
port: params.port,
cipher: params.method || "none",
uuid: params.password,
alterId: 0,
tls: params.obfs === "over-tls" || params.obfs === "wss",
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
};
if (proxy.tls) {
proxy.sni = params["obfs-host"] || params.server;
proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true");
}
// handle ws headers
if (params.obfs === "ws" || params.obfs === "wss") {
proxy.network = "ws";
proxy["ws-path"] = params["obfs-uri"];
proxy["ws-headers"] = {
Host: params["obfs-host"] || params.server, // if no host provided, use the same as server
};
}
return proxy;
};
return {name, test, parse};
}
function QX_Trojan() {
const name = "QX Trojan Parser";
const test = (line) => {
return /^trojan\s*=/.test(line.split(",")[0].trim());
};
const parse = (line) => {
const params = getQXParams(line);
const proxy = {
type: "trojan",
name: params.tag,
server: params.server,
port: params.port,
password: params.password,
sni: params["tls-host"] || params.server,
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
};
proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true");
return proxy;
};
return {name, test, parse};
}
function QX_Http() {
const name = "QX HTTP Parser";
const test = (line) => {
return /^http\s*=/.test(line.split(",")[0].trim());
};
const parse = (line) => {
const params = getQXParams(line);
const proxy = {
type: "http",
name: params.tag,
server: params.server,
port: params.port,
tls: JSON.parse(params["over-tls"] || "false"),
udp: JSON.parse(params["udp-relay"] || "false"),
tfo: JSON.parse(params["fast-open"] || "false"),
};
if (params.username && params.username !== 'none') proxy.username = params.username;
if (params.password && params.password !== 'none') proxy.password = params.password;
if (proxy.tls) {
proxy.sni = params["tls-host"] || proxy.server;
proxy['skip-cert-verify'] = !JSON.parse(params["tls-verification"] || "true");
}
return proxy;
};
return {name, test, parse};
}
function getQXParams(line) {
const groups = line.split(",");
const params = {};
const protocols = ["shadowsocks", "vmess", "http", "trojan"];
groups.forEach((g) => {
let [key, value] = g.split("=");
key = key.trim();
value = value.trim();
if (protocols.indexOf(key) !== -1) {
params.type = key;
const conf = value.split(":");
params.server = conf[0];
params.port = conf[1];
} else {
params[key.trim()] = value.trim();
}
});
return params;
}
function Loon_SS() {
const name = "Loon SS Parser";
const test = (line) => {
return (
line.split(",")[0].split("=")[1].trim().toLowerCase() === "shadowsocks"
);
};
const parse = (line) => {
const params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "ss",
server: params[1],
port: params[2],
cipher: params[3],
password: params[4].replace(/"/g, ""),
};
// handle obfs
if (params.length > 5) {
proxy.plugin = "obfs";
proxy["plugin-opts"] = {
mode: params[5],
host: params[6],
};
}
return proxy;
};
return {name, test, parse};
}
function Loon_SSR() {
const name = "Loon SSR Parser";
const test = (line) => {
return (
line.split(",")[0].split("=")[1].trim().toLowerCase() === "shadowsocksr"
);
};
const parse = (line) => {
const params = line.split("=")[1].split(",");
const supported = {
Surge: false,
};
return {
name: line.split("=")[0].trim(),
type: "ssr",
server: params[1],
port: params[2],
cipher: params[3],
password: params[4].replace(/"/g, ""),
protocol: params[5],
"protocol-param": params[6].match(/{(.*)}/)[1],
supported,
obfs: params[7],
"obfs-param": params[8].match(/{(.*)}/)[1],
};
};
return {name, test, parse};
}
function Loon_VMess() {
const name = "Loon VMess Parser";
const test = (line) => {
// distinguish between surge vmess
return (
/^.*=\s*vmess/i.test(line.split(",")[0]) &&
line.indexOf("username") === -1
);
};
const parse = (line) => {
let params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "vmess",
server: params[1],
port: params[2],
cipher: params[3] || "none",
uuid: params[4].replace(/"/g, ""),
alterId: 0,
};
// get transport options
params = params.splice(5);
for (const item of params) {
const [key, val] = item.split(":");
params[key] = val;
}
proxy.tls = JSON.parse(params["over-tls"] || "false");
if (proxy.tls) {
proxy.sni = params["tls-name"] || proxy.server;
proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false");
}
switch (params.transport) {
case "tcp":
break;
case "ws":
proxy.network = params.transport;
proxy["ws-path"] = params.path;
proxy["ws-headers"] = {
Host: params.host,
};
}
if (proxy.tls) {
proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false");
}
return proxy;
};
return {name, test, parse};
}
function Loon_Trojan() {
const name = "Loon Trojan Parser";
const test = (line) => {
return (
/^.*=\s*trojan/i.test(line.split(",")[0]) &&
line.indexOf("password") === -1
);
};
const parse = (line) => {
const params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "trojan",
server: params[1],
port: params[2],
password: params[3].replace(/"/g, ""),
sni: params[1], // default sni is the server itself
"skip-cert-verify": JSON.parse(params["skip-cert-verify"] || "false"),
};
// trojan sni
if (params.length > 4) {
const [key, val] = params[4].split(":");
if (key === "tls-name") proxy.sni = val;
else throw new Error(`Unknown option ${key} for line: \n${line}`);
}
return proxy;
};
return {name, test, parse};
}
function Loon_Http() {
const name = "Loon HTTP Parser";
const test = (line) => {
return (
/^.*=\s*http/i.test(line.split(",")[0]) &&
line.split(",").length === 5 &&
line.indexOf("username") === -1 &&
line.indexOf("password") === -1
);
};
const parse = (line) => {
const params = line.split("=")[1].split(",");
const proxy = {
name: line.split("=")[0].trim(),
type: "http",
server: params[1],
port: params[2],
tls: params[2] === "443", // port 443 is considered as https type
};
if (params[3]) proxy.username = params[3];
if (params[4]) proxy.password = params[4];
if (proxy.tls) {
proxy.sni = params["tls-name"] || proxy.server;
proxy['skip-cert-verify'] = JSON.parse(params["skip-cert-verify"] || "false");
}
return proxy;
};
return {name, test, parse};
}
function Surge_SS() {
const name = "Surge SS Parser";
const test = (line) => {
return /^.*=\s*ss/.test(line.split(",")[0]);
};
const parse = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "ss",
server: params.server,
port: params.port,
cipher: params["encrypt-method"],
password: params.password,
tfo: JSON.parse(params.tfo || "false"),
udp: JSON.parse(params["udp-relay"] || "false"),
};
// handle obfs
if (params.obfs) {
proxy.plugin = "obfs";
proxy["plugin-opts"] = {
mode: params.obfs,
host: params["obfs-host"],
};
}
return proxy;
};
return {name, test, parse};
}
function Surge_VMess() {
const name = "Surge VMess Parser";
const test = (line) => {
return (
/^.*=\s*vmess/.test(line.split(",")[0]) && line.indexOf("username") !== -1
);
};
const parse = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "vmess",
server: params.server,
port: params.port,
uuid: params.username,
alterId: 0, // surge does not have this field
cipher: "none", // surge does not have this field
tls: JSON.parse(params.tls || "false"),
tfo: JSON.parse(params.tfo || "false"),
};
if (proxy.tls) {
if (typeof params["skip-cert-verify"] !== "undefined") {
proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1";
}
proxy.sni = params["sni"] || params.server;
}
// use websocket
if (JSON.parse(params.ws || "false")) {
proxy.network = "ws";
proxy["ws-path"] = params["ws-path"];
const res = params["ws-headers"].match(/(,|^|\s)*HOST:\s*(.*?)(,|$)/);
const host = res ? res[2] : proxy.server;
proxy["ws-headers"] = {
Host: host || params.server,
};
}
return proxy;
};
return {name, test, parse};
}
function Surge_Trojan() {
const name = "Surge Trojan Parser";
const test = (line) => {
return (
/^.*=\s*trojan/.test(line.split(",")[0]) && line.indexOf("sni") !== -1
);
};
const parse = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "trojan",
server: params.server,
port: params.port,
password: params.password,
sni: params.sni || params.server,
tfo: JSON.parse(params.tfo || "false"),
};
if (typeof params["skip-cert-verify"] !== "undefined") {
proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1";
}
return proxy;
};
return {name, test, parse};
}
function Surge_Http() {
const name = "Surge HTTP Parser";
const test = (line) => {
return (
/^.*=\s*http/.test(line.split(",")[0]) && !Loon_Http().test(line)
);
};
const parse = (line) => {
const params = getSurgeParams(line);
const proxy = {
name: params.name,
type: "http",
server: params.server,
port: params.port,
tls: JSON.parse(params.tls || "false"),
tfo: JSON.parse(params.tfo || "false"),
};
if (proxy.tls) {
if (typeof params["skip-cert-verify"] !== "undefined") {
proxy['skip-cert-verify'] = params["skip-cert-verify"] === true || params["skip-cert-verify"] === "1";
}
proxy.sni = params.sni || params.server;
}
if (params.username && params.username !== "none") proxy.username = params.username;
if (params.password && params.password !== "none") proxy.password = params.password;
return proxy;
};
return {name, test, parse};
}
function getSurgeParams(line) {
const params = {};
params.name = line.split("=")[0].trim();
const segments = line.split(",");
params.server = segments[1].trim();
params.port = segments[2].trim();
for (let i = 3; i < segments.length; i++) {
const item = segments[i];
if (item.indexOf("=") !== -1) {
const [key, value] = item.split("=");
params[key.trim()] = value.trim();
}
}
return params;
}
return [
URI_SS(), URI_SSR(), URI_VMess(), URI_Trojan(),
Clash_All(),
Surge_SS(), Surge_VMess(), Surge_Trojan(), Surge_Http(),
Loon_SS(), Loon_SSR(), Loon_VMess(), Loon_Trojan(), Loon_Http(),
QX_SS(), QX_SSR(), QX_VMess(), QX_Trojan(), QX_Http()
];
})();
const PROXY_PROCESSORS = (function () {
// force to set some properties (e.g., skip-cert-verify, udp, tfo, etc.)
function SetPropertyOperator({key, value}) {
return {
name: "Set Property Operator",
func: (proxies) => {
return proxies.map((p) => {
p[key] = value;
return p;
});
},
};
}
// add or remove flag for proxies
function FlagOperator(add = true) {
return {
name: "Flag Operator",
func: (proxies) => {
return proxies.map((proxy) => {
if (!add) {
// no flag
proxy.name = removeFlag(proxy.name);
} else {
// get flag
const newFlag = getFlag(proxy.name);
// remove old flag
proxy.name = removeFlag(proxy.name);
proxy.name = newFlag + " " + proxy.name;
proxy.name = proxy.name.replace(/🇹🇼/g, "🇨🇳");
}
return proxy;
});
},
};
}
// sort proxies according to their names
function SortOperator(order = "asc") {
return {
name: "Sort Operator",
func: (proxies) => {
switch (order) {
case "asc":
case "desc":
return proxies.sort((a, b) => {
let res = a.name > b.name ? 1 : -1;
res *= order === "desc" ? -1 : 1;
return res;
});
case "random":
return shuffle(proxies);
default:
throw new Error("Unknown sort option: " + order);
}
},
};
}
// sort by regex
function RegexSortOperator(expressions) {
return {
name: "Regex Sort Operator",
func: (proxies) => {
expressions = expressions.map(expr => buildRegex(expr));
return proxies.sort((a, b) => {
const oA = getRegexOrder(expressions, a.name);
const oB = getRegexOrder(expressions, b.name);
if (oA && !oB) return -1;
if (oB && !oA) return 1;
if (oA && oB) return oA < oB ? -1 : 1;
if ((!oA && !oB) || (oA && oB && oA === oB))
return a.name < b.name ? -1 : 1; // fallback to normal sort
})
}
};
}
function getRegexOrder(expressions, str) {
let order = null;
for (let i = 0; i < expressions.length; i++) {
if (expressions[i].test(str)) {
order = i + 1; // plus 1 is important! 0 will be treated as false!!!
break;
}
}
return order;
}
// rename by regex
// keywords: [{expr: "string format regex", now: "now"}]
function RegexRenameOperator(regex) {
return {
name: "Regex Rename Operator",
func: (proxies) => {
return proxies.map((proxy) => {
for (const {expr, now} of regex) {
proxy.name = proxy.name.replace(buildRegex(expr, "g"), now).trim();
}
return proxy;
});
},
};
}
// delete regex operator
// regex: ['a', 'b', 'c']
function RegexDeleteOperator(regex) {
const regex_ = regex.map((r) => {
return {
expr: r,
now: "",
};
});
return {
name: "Regex Delete Operator",
func: RegexRenameOperator(regex_).func,
};
}
// use base64 encoded script to rename
/** Example script
function operator(proxies) {
// do something
return proxies;
}
WARNING:
1. This function name should be `operator`!
2. Always declare variables before using them!
*/
function ScriptOperator(script) {
return {
name: "Script Operator",
func: (proxies) => {
let output = proxies;
(function () {
// interface to get internal operators
const $get = (name, args) => {
const item = PROXY_PROCESSORS[name];
return item(args);
};
const $process = ApplyProcessor;
eval(script);
output = operator(proxies);
})();
return output;
},
};
}
/**************************** Filters ***************************************/
// filter useless proxies
function UselessFilter() {
const KEYWORDS = [
"网址",
"流量",
"时间",
"应急",
"过期",
"Bandwidth",
"expire",
];
return {
name: "Useless Filter",
func: RegexFilter({
regex: KEYWORDS,
keep: false,
}).func,
};
}
// filter by regions
function RegionFilter(regions) {
const REGION_MAP = {
HK: "🇭🇰",
TW: "🇹🇼",
US: "🇺🇸",
SG: "🇸🇬",
JP: "🇯🇵",
UK: "🇬🇧",
};
return {
name: "Region Filter",
func: (proxies) => {
// this would be high memory usage
return proxies.map((proxy) => {
const flag = getFlag(proxy.name);
return regions.some((r) => REGION_MAP[r] === flag);
});
},
};
}
// filter by regex
function RegexFilter({regex = [], keep = true}) {
return {
name: "Regex Filter",
func: (proxies) => {
return proxies.map((proxy) => {
const selected = regex.some((r) => {
return buildRegex(r).test(proxy.name);
});
return keep ? selected : !selected;
});
},
};
}
// filter by proxy types
function TypeFilter(types) {
return {
name: "Type Filter",
func: (proxies) => {
return proxies.map((proxy) => types.some((t) => proxy.type === t));
},
};
}
// use base64 encoded script to filter proxies
/**
Script Example
function func(proxies) {
const selected = FULL(proxies.length, true);
// do something
return selected;
}
WARNING:
1. This function name should be `func`!
2. Always declare variables before using them!
*/
function ScriptFilter(script) {
return {
name: "Script Filter",
func: (proxies) => {
let output = FULL(proxies.length, true);
!(function () {
eval(script);
output = filter(proxies);
})();
return output;
},
};
}
/******************************** Utility Functions *********************************************/
// get proxy flag according to its name
function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
const flags = {
"🇦🇨": ["AC"],
"🇦🇹": ["奥地利", "维也纳"],
"🇦🇺": ["AU", "Australia", "Sydney", "澳大利亚", "澳洲", "墨尔本", "悉尼"],
"🇧🇪": ["BE", "比利时"],
"🇧🇬": ["保加利亚", "Bulgaria"],
"🇧🇷": ["BR", "Brazil", "巴西", "圣保罗"],
"🇨🇦": [
"CA",
"Canada",
"Waterloo",
"加拿大",
"蒙特利尔",
"温哥华",
"楓葉",
"枫叶",
"滑铁卢",
"多伦多",
],
"🇨🇭": ["瑞士", "苏黎世", "Switzerland"],
"🇩🇪": ["DE", "German", "GERMAN", "德国", "德國", "法兰克福"],
"🇩🇰": ["丹麦"],
"🇪🇸": ["ES", "西班牙", "Spain"],
"🇪🇺": ["EU", "欧盟", "欧罗巴"],
"🇫🇮": ["Finland", "芬兰", "赫尔辛基"],
"🇫🇷": ["FR", "France", "法国", "法國", "巴黎"],
"🇬🇧": ["UK", "GB", "England", "United Kingdom", "英国", "伦敦", "英"],
"🇲🇴": ["MO", "Macao", "澳门", "CTM"],
"🇭🇺": ["匈牙利", "Hungary"],
"🇭🇰": [
"HK",
"Hongkong",
"Hong Kong",
"香港",
"深港",
"沪港",
"呼港",
"HKT",
"HKBN",
"HGC",
"WTT",
"CMI",
"穗港",
"京港",
"港",
],
"🇮🇩": ["Indonesia", "印尼", "印度尼西亚", "雅加达"],
"🇮🇪": ["Ireland", "爱尔兰", "都柏林"],
"🇮🇳": ["India", "印度", "孟买", "Mumbai"],
"🇰🇵": ["KP", "朝鲜"],
"🇰🇷": ["KR", "Korea", "KOR", "韩国", "首尔", "韩", "韓"],
"🇱🇻": ["Latvia", "Latvija", "拉脱维亚"],
"🇲🇽️": ["MEX", "MX", "墨西哥"],
"🇲🇾": ["MY", "Malaysia", "马来西亚", "吉隆坡"],
"🇳🇱": ["NL", "Netherlands", "荷兰", "荷蘭", "尼德蘭", "阿姆斯特丹"],
"🇵🇭": ["PH", "Philippines", "菲律宾"],
"🇷🇴": ["RO", "罗马尼亚"],
"🇷🇺": [
"RU",
"Russia",
"俄罗斯",
"俄羅斯",
"伯力",
"莫斯科",
"圣彼得堡",
"西伯利亚",
"新西伯利亚",
"京俄",
"杭俄",
],
"🇸🇦": ["沙特", "迪拜"],
"🇸🇪": ["SE", "Sweden"],
"🇸🇬": [
"SG",
"Singapore",
"新加坡",
"狮城",
"沪新",
"京新",
"泉新",
"穗新",
"深新",
"杭新",
"广新",
],
"🇹🇭": ["TH", "Thailand", "泰国", "泰國", "曼谷"],
"🇹🇷": ["TR", "Turkey", "土耳其", "伊斯坦布尔"],
"🇹🇼": [
"TW",
"Taiwan",
"台湾",
"台北",
"台中",
"新北",
"彰化",
"CHT",
"台",
"HINET",
],
"🇺🇸": [
"US",
"USA",
"America",
"United States",
"美国",
"美",
"京美",
"波特兰",
"达拉斯",
"俄勒冈",
"凤凰城",
"费利蒙",
"硅谷",
"矽谷",
"拉斯维加斯",
"洛杉矶",
"圣何塞",
"圣克拉拉",
"西雅图",
"芝加哥",
"沪美",
"哥伦布",
"纽约",
],
"🇻🇳": ["VN", "越南", "胡志明市"],
"🇮🇹": ["Italy", "IT", "Nachash", "意大利", "米兰", "義大利"],
"🇿🇦": ["South Africa", "南非"],
"🇦🇪": ["United Arab Emirates", "阿联酋"],
"🇯🇵": [
"JP",
"Japan",
"日",
"日本",
"东京",
"大阪",
"埼玉",
"沪日",
"穗日",
"川日",
"中日",
"泉日",
"杭日",
"深日",
"辽日",
"广日",
],
"🇦🇷": ["AR", "阿根廷"],
"🇳🇴": ["Norway", "挪威", "NO"],
"🇨🇳": [
"CN",
"China",
"回国",
"中国",
"江苏",
"北京",
"上海",
"广州",
"深圳",
"杭州",
"徐州",
"青岛",
"宁波",
"镇江",
"back",
],
"🏳️‍🌈": ["流量", "时间", "应急", "过期", "Bandwidth", "expire"],
};
for (let k of Object.keys(flags)) {
if (flags[k].some((item) => name.indexOf(item) !== -1)) {
return k;
}
}
// no flag found
const oldFlag = (name.match(
/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/
) || [])[0];
return oldFlag || "🏴‍☠️";
}
// remove flag
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, "")
.trim();
}
// shuffle array
function shuffle(array) {
let currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
return {
"Useless Filter": UselessFilter,
"Region Filter": RegionFilter,
"Regex Filter": RegexFilter,
"Type Filter": TypeFilter,
"Script Filter": ScriptFilter,
"Set Property Operator": SetPropertyOperator,
"Flag Operator": FlagOperator,
"Sort Operator": SortOperator,
"Regex Sort Operator": RegexSortOperator,
"Regex Rename Operator": RegexRenameOperator,
"Regex Delete Operator": RegexDeleteOperator,
"Script Operator": ScriptOperator,
};
})();
const PROXY_PRODUCERS = (function () {
function QX_Producer() {
const targetPlatform = "QX";
const produce = (proxy) => {
let obfs_opts;
let tls_opts;
switch (proxy.type) {
case "ss":
obfs_opts = "";
if (proxy.plugin === "obfs") {
const {host, mode} = proxy['plugin-opts'];
obfs_opts = `,obfs=${mode}${
host ? ",obfs-host=" + host : ""
}`;
}
if (proxy.plugin === "v2ray-plugin") {
const {tls, host, path} = proxy["plugin-opts"];
obfs_opts = `,obfs=${tls ? "wss" : "ws"}${
host ? ",obfs-host=" + host : ""
}${
path ? ",obfs-uri=" + path : ""
}`;
}
return `shadowsocks=${proxy.server}:${proxy.port},method=${
proxy.cipher
},password=${proxy.password}${obfs_opts}${
proxy.tfo ? ",fast-open=true" : ",fast-open=false"
}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${
proxy.name
}`;
case "ssr":
return `shadowsocks=${proxy.server}:${proxy.port},method=${
proxy.cipher
},password=${proxy.password},ssr-protocol=${proxy.protocol}${
proxy["protocol-param"]
? ",ssr-protocol-param=" + proxy["protocol-param"]
: ""
}${proxy.obfs ? ",obfs=" + proxy.obfs : ""}${
proxy["obfs-param"] ? ",obfs-host=" + proxy["obfs-param"] : ""
}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${
proxy.udp ? ",udp-relay=true" : ",udp-relay=false"
},tag=${proxy.name}`;
case "vmess":
obfs_opts = "";
if (proxy.network === "ws") {
// websocket
if (proxy.tls) {
// ws-tls
obfs_opts = `,obfs=wss${
proxy.sni ? ",obfs-host=" + proxy.sni : ""
}${
proxy["ws-path"] ? ",obfs-uri=" + proxy["ws-path"] : ""
},tls-verification=${proxy['skip-cert-verify'] ? "false" : "true"}`;
} else {
// ws
obfs_opts = `,obfs=ws${
proxy["ws-headers"].Host ? ",obfs-host=" + proxy["ws-headers"].Host : ""
}${
proxy["ws-path"] ? ",obfs-uri=" + proxy["ws-path"] : ""
}`;
}
} else {
// tcp
if (proxy.tls) {
obfs_opts = `,obfs=over-tls${
proxy.sni ? ",obfs-host=" + proxy.sni : ""
},tls-verification=${proxy['skip-cert-verify'] ? "false" : "true"}`;
}
}
return `vmess=${proxy.server}:${proxy.port},method=${
proxy.cipher === "auto" ? "none" : proxy.cipher
},password=${proxy.uuid}${obfs_opts}${
proxy.tfo ? ",fast-open=true" : ",fast-open=false"
}${proxy.udp ? ",udp-relay=true" : ",udp-relay=false"},tag=${
proxy.name
}`;
case "trojan":
return `trojan=${proxy.server}:${proxy.port},password=${
proxy.password
}${proxy.sni ? ",tls-host=" + proxy.sni : ""},over-tls=true,tls-verification=${
proxy['skip-cert-verify'] ? "false" : "true"
}${proxy.tfo ? ",fast-open=true" : ",fast-open=false"}${
proxy.udp ? ",udp-relay=true" : ",udp-relay=false"
},tag=${proxy.name}`;
case "http":
tls_opts = "";
if (proxy.tls) {
tls_opts = `,over-tls=true,tls-verification=${
proxy['skip-cert-verify'] ? "false" : "true"
}${
proxy.sni ? ",tls-host=" + proxy.sni : ""
}`;
}
return `http=${proxy.server}:${proxy.port},username=${
proxy.username
},password=${proxy.password}${tls_opts}${
proxy.tfo ? ",fast-open=true" : ",fast-open=false"
},tag=${proxy.name}`;
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`
);
};
return {produce};
}
function Loon_Producer() {
const targetPlatform = "Loon";
const produce = (proxy) => {
let obfs_opts, tls_opts;
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)) {
$.info(`Pre-processor [${processor.name}] activated`);
return processor.parse(raw);
}
} catch (e) {
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
}
}
return raw;
}
function safeMatch(p, line) {
let patternMatched;
try {
patternMatched = p.test(line);
} catch (err) {
patternMatched = false;
}
return patternMatched;
}
function parse(raw) {
raw = preprocess(raw);
// parse
const lines = raw.split("\n");
const proxies = [];
let lastParser;
for (let line of lines) {
line = line.trim();
if (line.length === 0) continue; // skip empty line
let matched = lastParser && safeMatch(lastParser, line);
if (!matched) {
for (const parser of PROXY_PARSERS) {
if (safeMatch(parser, line)) {
lastParser = parser;
matched = true;
$.info(`Proxy parser: ${parser.name} is activated`);
break;
}
}
}
if (!matched) {
$.error(`Failed to find a rule to parse line: \n${line}\n`);
} else {
try {
const proxy = lastParser.parse(line);
if (!proxy) {
$.error(`Parser ${lastParser.name} return nothing for \n${line}\n`);
}
proxies.push(proxy);
} catch (err) {
$.error(
`Failed to parse line: \n ${line}\n Reason: ${err.stack}`
);
}
}
}
return proxies;
}
async function process(proxies, operators = []) {
for (const item of operators) {
// process script
let script;
if (item.type.indexOf("Script") !== -1) {
const {mode, content} = item.args;
if (mode === "link") {
// if this is remote script, download it
try {
script = await $.http
.get(content)
.then((resp) => resp.body);
} catch (err) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`
);
// skip the script if download failed.
continue;
}
} else {
script = content;
}
}
if (!PROXY_PROCESSORS[item.type]) {
$.error(`Unknown operator: "${item.type}"`);
continue;
}
$.info(
`Applying "${item.type}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || "None"
}`
);
let processor;
if (item.type.indexOf('Script') !== -1) {
processor = PROXY_PROCESSORS[item.type](script);
} else {
processor = PROXY_PROCESSORS[item.type](item.args);
}
proxies = ApplyProcessor(processor, proxies);
}
return proxies;
}
function produce(proxies, targetPlatform) {
const producer = PROXY_PRODUCERS[targetPlatform];
if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
}
// filter unsupported proxies
proxies = proxies.filter(proxy => !(proxy.supported && proxy.supported[targetPlatform] === false));
$.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === "undefined" || producer.type === 'SINGLE') {
return proxies
.map(proxy => {
try {
return producer.produce(proxy);
} catch (err) {
$.error(
`Cannot produce proxy: ${JSON.stringify(
proxy, null, 2
)}\nReason: ${err}`
);
return "";
}
})
.filter(line => line.length > 0)
.join("\n");
} else if (producer.type === "ALL") {
return producer.produce(proxies);
}
}
return {
parse, process, produce
}
})();
/****************************************** Rule Utils **********************************************************/
var RuleUtils = (function () {
const RULE_TYPES_MAPPING = [
[/^(DOMAIN|host|HOST)$/, "DOMAIN"],
[/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, "DOMAIN-KEYWORD"],
[/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, "DOMAIN-SUFFIX"],
[/^USER-AGENT$/i, "USER-AGENT"],
[/^PROCESS-NAME$/, "PROCESS-NAME"],
[/^(DEST-PORT|DST-PORT)$/, "DST-PORT"],
[/^SRC-IP(-CIDR)?$/, "SRC-IP"],
[/^(IN|SRC)-PORT$/, "IN-PORT"],
[/^PROTOCOL$/, "PROTOCOL"],
[/^IP-CIDR$/i, "IP-CIDR"],
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/]
];
const RULE_PREPROCESSORS = (function () {
function HTML() {
const name = "HTML";
const test = raw => /^<!DOCTYPE html>/.test(raw);
// simply discard HTML
const parse = _ => "";
return {name, test, parse};
}
function ClashProvider() {
const name = "Clash Provider";
const test = raw => raw.indexOf("payload:") === 0
const parse = raw => {
return raw
.replace("payload:", "")
.replace(/^\s*-\s*/gm, "");
}
return {name, test, parse}
}
return [HTML(), ClashProvider()];
})();
const RULE_PARSERS = (function () {
function AllRuleParser() {
const name = "Universal Rule Parser";
const test = () => true;
const parse = (raw) => {
const lines = raw.split("\n");
const result = [];
for (let line of lines) {
line = line.trim();
// skip empty line
if (line.length === 0) continue;
// skip comments
if (/\s*#/.test(line)) continue;
try {
const params = line.split(",").map(w => w.trim());
let rawType = params[0];
let matched = false;
for (const item of RULE_TYPES_MAPPING) {
const regex = item[0];
if (regex.test(rawType)) {
matched = true;
const rule = {
type: item[1],
content: params[1],
};
if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") {
rule.options = params.slice(2)
}
result.push(rule);
}
}
if (!matched) throw new Error("Invalid rule type: " + rawType);
} catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
}
}
return result;
}
return {name, test, parse};
}
return [AllRuleParser()];
})();
const RULE_PROCESSORS = (function () {
function RegexFilter({regex = [], keep = true}) {
return {
name: "Regex Filter",
func: (rules) => {
return rules.map((rule) => {
const selected = regex.some((r) => {
r = new RegExp(r);
return r.test(rule);
});
return keep ? selected : !selected;
});
},
};
}
function TypeFilter(types) {
return {
name: "Type Filter",
func: (rules) => {
return rules.map((rule) => types.some((t) => rule.type === t));
},
};
}
function RemoveDuplicateFilter() {
return {
name: "Remove Duplicate Filter",
func: rules => {
const seen = new Set();
const result = [];
rules.forEach(rule => {
const options = rule.options || [];
options.sort();
const key = `${rule.type},${rule.content},${JSON.stringify(options)}`;
if (!seen.has(key)) {
result.push(rule)
seen.add(key);
}
});
return result;
}
}
}
// regex: [{expr: "string format regex", now: "now"}]
function RegexReplaceOperator(regex) {
return {
name: "Regex Rename Operator",
func: (rules) => {
return rules.map((rule) => {
for (const {expr, now} of regex) {
rule.content = rule.content.replace(new RegExp(expr, "g"), now).trim();
}
return rule;
});
},
};
}
return {
"Regex Filter": RegexFilter,
"Remove Duplicate Filter": RemoveDuplicateFilter,
"Type Filter": TypeFilter,
"Regex Replace Operator": RegexReplaceOperator
};
})();
const RULE_PRODUCERS = (function () {
function QXFilter() {
const type = "SINGLE";
const func = (rule) => {
// skip unsupported rules
const UNSUPPORTED = [
"URL-REGEX", "DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL"
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
const TRANSFORM = {
"DOMAIN-KEYWORD": "HOST-KEYWORD",
"DOMAIN-SUFFIX": "HOST-SUFFIX",
"DOMAIN": "HOST",
"IP-CIDR6": "IP6-CIDR"
};
// QX does not support the no-resolve option
return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`;
}
return {type, func};
}
function SurgeRuleSet() {
const type = "SINGLE";
const func = (rule) => {
let output = `${rule.type},${rule.content}`;
if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") {
output += rule.options ? `,${rule.options[0]}` : "";
}
return output;
}
return {type, func};
}
function LoonRules() {
const type = "SINGLE";
const func = (rule) => {
// skip unsupported rules
const UNSUPPORTED = [
"DEST-PORT", "SRC-IP", "IN-PORT", "PROTOCOL"
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
return SurgeRuleSet().func(rule);
}
return {type, func};
}
function ClashRuleProvider() {
const type = "ALL";
const func = (rules) => {
const TRANSFORM = {
"DEST-PORT": "DST-PORT",
"SRC-IP": "SRC-IP-CIDR",
"IN-PORT": "SRC-PORT"
};
const conf = {
payload: rules.map(rule => {
let output = `${TRANSFORM[rule.type] || rule.type},${rule.content}`;
if (rule.type === "IP-CIDR" || rule.type === "IP-CIDR6") {
output += rule.options ? `,${rule.options[0]}` : "";
}
return output;
})
}
return YAML.stringify(conf);
}
return {type, func};
}
return {
"QX": QXFilter(),
"Surge": SurgeRuleSet(),
"Loon": LoonRules(),
"Clash": ClashRuleProvider()
};
})();
function preprocess(raw) {
for (const processor of RULE_PREPROCESSORS) {
try {
if (processor.test(raw)) {
$.info(`Pre-processor [${processor.name}] activated`);
return processor.parse(raw);
}
} catch (e) {
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
}
}
return raw;
}
function parse(raw) {
raw = preprocess(raw);
for (const parser of RULE_PARSERS) {
let matched;
try {
matched = parser.test(raw);
} catch (err) {
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};
})();
function getBuiltInRules() {
return {
"AD": {
"name": "AD",
"description": "",
"urls": [
"https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-surge.txt",
"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Providers/BanAD.yaml",
]
},
"Global": {
"name": "Global",
"description": "",
"urls": [
"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Providers/ProxyGFWlist.yaml",
"https://raw.githubusercontent.com/DivineEngine/Profiles/master/Quantumult/Filter/Global.list"
]
},
"CN": {
"name": "CN",
"description": "",
"urls": [
"https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/Providers/ChinaDomain.yaml",
"https://raw.githubusercontent.com/DivineEngine/Profiles/master/Quantumult/Filter/China.list"
]
}
};
}
/****************************************** 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);
}
// utils functions
function clone(object) {
return JSON.parse(JSON.stringify(object));
}
function buildRegex(str, ...options) {
options = options.join("");
if (str.startsWith("(?i)")) {
str = str.substr(4);
return new RegExp(str, 'i' + options);
} else {
return new RegExp(str, options);
}
}
/****************************************** 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, url: options.url, headers: options.headers, body: options.body});
} 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({token, key}) {
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 === key) {
return g.id;
}
}
return -1;
});
}
this.upload = async function ({filename, content}) {
const id = await locate();
const files = {
[filename]: {content}
};
if (id === -1) {
// create a new gist for backup
return http.post({
url: "/gists",
body: JSON.stringify({
description: key,
public: false,
files
})
});
} else {
// update an existing gist
return http.patch({
url: `/gists/${id}`,
body: JSON.stringify({files})
});
}
};
this.download = async function (filename) {
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[filename].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, () => {
$.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);
// 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(/&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
*/
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;
},
};
})();