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