Bump to ES6

This commit is contained in:
Peng-YM 2022-05-24 21:20:26 +08:00
parent 46e37df110
commit e228416718
23 changed files with 14866 additions and 3442 deletions

7
backend/.babelrc Normal file
View File

@ -0,0 +1,7 @@
{
"presets": [
[
"@babel/preset-env"
]
]
}

6
backend/.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 4,
"bracketSpacing": true
}

View File

@ -5,8 +5,9 @@
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝ * ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗ * ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash. * Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
* @version: 1.5 * @updated: <%= updated %>
* @version: <%= pkg.version %>
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/Peng-YM/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46

48
backend/gulpfile.babel.js Normal file
View File

@ -0,0 +1,48 @@
import fs from 'fs';
import browserify from 'browserify';
import gulp from 'gulp';
import prettier from 'gulp-prettier';
import header from 'gulp-header';
const DEST_FILE = 'sub-store.min.js';
export function styles() {
return gulp
.src('src/**/*.js')
.pipe(
prettier({
singleQuote: true,
trailingComma: 'all',
tabWidth: 4,
bracketSpacing: true
})
)
.pipe(gulp.dest((file) => file.base));
}
export function scripts() {
return browserify('src/main.js')
.transform('babelify', {
presets: [
[
'@babel/preset-env'
]
]
})
.plugin('tinyify')
.bundle()
.pipe(fs.createWriteStream(DEST_FILE));
}
export function banner() {
const pkg = require('./package.json');
return gulp
.src(DEST_FILE)
.pipe(header(fs.readFileSync('./banner', 'utf-8'), { pkg, updated: new Date().toLocaleString() }))
.pipe(gulp.dest((file) => file.base));
}
const build = gulp.series(styles, scripts, banner);
export default build;

10843
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "1.5", "version": "1.5.1",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"serve": "node src/main.js", "serve": "node src/main.js",
"build": "browserify -p tinyify src/main.js > bundle && cat banner bundle > sub-store.min.js && rm bundle" "build": "gulp"
}, },
"author": "Peng-YM", "author": "Peng-YM",
"license": "GPL", "license": "GPL",
@ -18,8 +18,20 @@
"static-js-yaml": "^1.0.0" "static-js-yaml": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.0",
"@babel/preset-env": "^7.18.0",
"@babel/register": "^7.17.7",
"@types/gulp": "^4.0.9",
"axios": "^0.20.0", "axios": "^0.20.0",
"babelify": "^10.0.0",
"browser-pack-flat": "^3.4.2",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-header": "^2.0.9",
"gulp-prettier": "^4.0.0",
"prettier": "2.6.2",
"prettier-plugin-sort-imports": "^1.6.1",
"tinyify": "^3.0.0" "tinyify": "^3.0.0"
} }
} }

View File

@ -1,4 +1,4 @@
const { API } = require('../utils/open-api'); import { API } from '../utils/open-api';
const $ = API('sub-store'); const $ = API('sub-store');
module.exports = $; export default $;

File diff suppressed because it is too large Load Diff

View File

@ -1,293 +1,322 @@
const $ = require("./app"); import $ from './app';
const RULE_TYPES_MAPPING = [ const RULE_TYPES_MAPPING = [
[ /^(DOMAIN|host|HOST)$/, 'DOMAIN' ], [/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
[ /^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD' ], [/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],
[ /^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX' ], [/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],
[ /^USER-AGENT$/i, 'USER-AGENT' ], [/^USER-AGENT$/i, 'USER-AGENT'],
[ /^PROCESS-NAME$/, 'PROCESS-NAME' ], [/^PROCESS-NAME$/, 'PROCESS-NAME'],
[ /^(DEST-PORT|DST-PORT)$/, 'DST-PORT' ], [/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],
[ /^SRC-IP(-CIDR)?$/, 'SRC-IP' ], [/^SRC-IP(-CIDR)?$/, 'SRC-IP'],
[ /^(IN|SRC)-PORT$/, 'IN-PORT' ], [/^(IN|SRC)-PORT$/, 'IN-PORT'],
[ /^PROTOCOL$/, 'PROTOCOL' ], [/^PROTOCOL$/, 'PROTOCOL'],
[ /^IP-CIDR$/i, 'IP-CIDR' ], [/^IP-CIDR$/i, 'IP-CIDR'],
[ /^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/ ] [/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
]; ];
const RULE_PREPROCESSORS = (function() { const RULE_PREPROCESSORS = (function () {
function HTML() { function HTML() {
const name = 'HTML'; const name = 'HTML';
const test = (raw) => /^<!DOCTYPE html>/.test(raw); const test = (raw) => /^<!DOCTYPE html>/.test(raw);
// simply discard HTML // simply discard HTML
const parse = (_) => ''; const parse = (_) => '';
return { name, test, parse }; return { name, test, parse };
} }
function ClashProvider() { function ClashProvider() {
const name = 'Clash Provider'; const name = 'Clash Provider';
const test = (raw) => raw.indexOf('payload:') === 0; const test = (raw) => raw.indexOf('payload:') === 0;
const parse = (raw) => { const parse = (raw) => {
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, ''); return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
}; };
return { name, test, parse }; return { name, test, parse };
} }
return [ HTML(), ClashProvider() ]; return [HTML(), ClashProvider()];
})(); })();
const RULE_PARSERS = (function() { const RULE_PARSERS = (function () {
function AllRuleParser() { function AllRuleParser() {
const name = 'Universal Rule Parser'; const name = 'Universal Rule Parser';
const test = () => true; const test = () => true;
const parse = (raw) => { const parse = (raw) => {
const lines = raw.split('\n'); const lines = raw.split('\n');
const result = []; const result = [];
for (let line of lines) { for (let line of lines) {
line = line.trim(); line = line.trim();
// skip empty line // skip empty line
if (line.length === 0) continue; if (line.length === 0) continue;
// skip comments // skip comments
if (/\s*#/.test(line)) continue; if (/\s*#/.test(line)) continue;
try { try {
const params = line.split(',').map((w) => w.trim()); const params = line.split(',').map((w) => w.trim());
let rawType = params[0]; let rawType = params[0];
let matched = false; let matched = false;
for (const item of RULE_TYPES_MAPPING) { for (const item of RULE_TYPES_MAPPING) {
const regex = item[0]; const regex = item[0];
if (regex.test(rawType)) { if (regex.test(rawType)) {
matched = true; matched = true;
const rule = { const rule = {
type: item[1], type: item[1],
content: params[1] content: params[1],
}; };
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') { if (
rule.options = params.slice(2); rule.type === 'IP-CIDR' ||
} rule.type === 'IP-CIDR6'
result.push(rule); ) {
} rule.options = params.slice(2);
} }
if (!matched) throw new Error('Invalid rule type: ' + rawType); result.push(rule);
} catch (e) { }
console.error(`Failed to parse line: ${line}\n Reason: ${e}`); }
} if (!matched)
} throw new Error('Invalid rule type: ' + rawType);
return result; } catch (e) {
}; console.error(
return { name, test, parse }; `Failed to parse line: ${line}\n Reason: ${e}`,
} );
}
}
return result;
};
return { name, test, parse };
}
return [ AllRuleParser() ]; return [AllRuleParser()];
})(); })();
const RULE_PROCESSORS = (function() { const RULE_PROCESSORS = (function () {
function RegexFilter({ regex = [], keep = true }) { function RegexFilter({ regex = [], keep = true }) {
return { return {
name: 'Regex Filter', name: 'Regex Filter',
func: (rules) => { func: (rules) => {
return rules.map((rule) => { return rules.map((rule) => {
const selected = regex.some((r) => { const selected = regex.some((r) => {
r = new RegExp(r); r = new RegExp(r);
return r.test(rule); return r.test(rule);
}); });
return keep ? selected : !selected; return keep ? selected : !selected;
}); });
} },
}; };
} }
function TypeFilter(types) { function TypeFilter(types) {
return { return {
name: 'Type Filter', name: 'Type Filter',
func: (rules) => { func: (rules) => {
return rules.map((rule) => types.some((t) => rule.type === t)); return rules.map((rule) => types.some((t) => rule.type === t));
} },
}; };
} }
function RemoveDuplicateFilter() { function RemoveDuplicateFilter() {
return { return {
name: 'Remove Duplicate Filter', name: 'Remove Duplicate Filter',
func: (rules) => { func: (rules) => {
const seen = new Set(); const seen = new Set();
const result = []; const result = [];
rules.forEach((rule) => { rules.forEach((rule) => {
const options = rule.options || []; const options = rule.options || [];
options.sort(); options.sort();
const key = `${rule.type},${rule.content},${JSON.stringify(options)}`; const key = `${rule.type},${rule.content},${JSON.stringify(
if (!seen.has(key)) { options,
result.push(rule); )}`;
seen.add(key); if (!seen.has(key)) {
} result.push(rule);
}); seen.add(key);
return result; }
} });
}; return result;
} },
};
}
// regex: [{expr: "string format regex", now: "now"}] // regex: [{expr: "string format regex", now: "now"}]
function RegexReplaceOperator(regex) { function RegexReplaceOperator(regex) {
return { return {
name: 'Regex Rename Operator', name: 'Regex Rename Operator',
func: (rules) => { func: (rules) => {
return rules.map((rule) => { return rules.map((rule) => {
for (const { expr, now } of regex) { for (const { expr, now } of regex) {
rule.content = rule.content.replace(new RegExp(expr, 'g'), now).trim(); rule.content = rule.content
} .replace(new RegExp(expr, 'g'), now)
return rule; .trim();
}); }
} return rule;
}; });
} },
};
}
return { return {
'Regex Filter': RegexFilter, 'Regex Filter': RegexFilter,
'Remove Duplicate Filter': RemoveDuplicateFilter, 'Remove Duplicate Filter': RemoveDuplicateFilter,
'Type Filter': TypeFilter, 'Type Filter': TypeFilter,
'Regex Replace Operator': RegexReplaceOperator 'Regex Replace Operator': RegexReplaceOperator,
}; };
})(); })();
const RULE_PRODUCERS = (function() { const RULE_PRODUCERS = (function () {
function QXFilter() { function QXFilter() {
const type = 'SINGLE'; const type = 'SINGLE';
const func = (rule) => { const func = (rule) => {
// skip unsupported rules // skip unsupported rules
const UNSUPPORTED = [ 'URL-REGEX', 'DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL' ]; const UNSUPPORTED = [
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; 'URL-REGEX',
'DEST-PORT',
'SRC-IP',
'IN-PORT',
'PROTOCOL',
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
const TRANSFORM = { const TRANSFORM = {
'DOMAIN-KEYWORD': 'HOST-KEYWORD', 'DOMAIN-KEYWORD': 'HOST-KEYWORD',
'DOMAIN-SUFFIX': 'HOST-SUFFIX', 'DOMAIN-SUFFIX': 'HOST-SUFFIX',
DOMAIN: 'HOST', DOMAIN: 'HOST',
'IP-CIDR6': 'IP6-CIDR' 'IP-CIDR6': 'IP6-CIDR',
}; };
// QX does not support the no-resolve option // QX does not support the no-resolve option
return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`; return `${TRANSFORM[rule.type] || rule.type},${
}; rule.content
return { type, func }; },SUB-STORE`;
} };
return { type, func };
}
function SurgeRuleSet() { function SurgeRuleSet() {
const type = 'SINGLE'; const type = 'SINGLE';
const func = (rule) => { const func = (rule) => {
let output = `${rule.type},${rule.content}`; let output = `${rule.type},${rule.content}`;
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') { if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
output += rule.options ? `,${rule.options[0]}` : ''; output += rule.options ? `,${rule.options[0]}` : '';
} }
return output; return output;
}; };
return { type, func }; return { type, func };
} }
function LoonRules() { function LoonRules() {
const type = 'SINGLE'; const type = 'SINGLE';
const func = (rule) => { const func = (rule) => {
// skip unsupported rules // skip unsupported rules
const UNSUPPORTED = [ 'DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL' ]; const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
return SurgeRuleSet().func(rule); return SurgeRuleSet().func(rule);
}; };
return { type, func }; return { type, func };
} }
function ClashRuleProvider() { function ClashRuleProvider() {
const type = 'ALL'; const type = 'ALL';
const func = (rules) => { const func = (rules) => {
const TRANSFORM = { const TRANSFORM = {
'DEST-PORT': 'DST-PORT', 'DEST-PORT': 'DST-PORT',
'SRC-IP': 'SRC-IP-CIDR', 'SRC-IP': 'SRC-IP-CIDR',
'IN-PORT': 'SRC-PORT' 'IN-PORT': 'SRC-PORT',
}; };
const conf = { const conf = {
payload: rules.map((rule) => { payload: rules.map((rule) => {
let output = `${TRANSFORM[rule.type] || rule.type},${rule.content}`; let output = `${TRANSFORM[rule.type] || rule.type},${
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') { rule.content
output += rule.options ? `,${rule.options[0]}` : ''; }`;
} if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
return output; output += rule.options ? `,${rule.options[0]}` : '';
}) }
}; return output;
return YAML.stringify(conf); }),
}; };
return { type, func }; return YAML.stringify(conf);
} };
return { type, func };
}
return { return {
QX: QXFilter(), QX: QXFilter(),
Surge: SurgeRuleSet(), Surge: SurgeRuleSet(),
Loon: LoonRules(), Loon: LoonRules(),
Clash: ClashRuleProvider() Clash: ClashRuleProvider(),
}; };
})(); })();
const RuleUtils = (function() { export const RuleUtils = (function () {
function preprocess(raw) { function preprocess(raw) {
for (const processor of RULE_PREPROCESSORS) { for (const processor of RULE_PREPROCESSORS) {
try { try {
if (processor.test(raw)) { if (processor.test(raw)) {
$.info(`Pre-processor [${processor.name}] activated`); $.info(`Pre-processor [${processor.name}] activated`);
return processor.parse(raw); return processor.parse(raw);
} }
} catch (e) { } catch (e) {
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`); $.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
} }
} }
return raw; return raw;
} }
function parse(raw) { function parse(raw) {
raw = preprocess(raw); raw = preprocess(raw);
for (const parser of RULE_PARSERS) { for (const parser of RULE_PARSERS) {
let matched; let matched;
try { try {
matched = parser.test(raw); matched = parser.test(raw);
} catch (err) { } catch (err) {
matched = false; matched = false;
} }
if (matched) { if (matched) {
$.info(`Rule parser [${parser.name}] is activated!`); $.info(`Rule parser [${parser.name}] is activated!`);
return parser.parse(raw); return parser.parse(raw);
} }
} }
} }
async function process(rules, operators) { async function process(rules, operators) {
for (const item of operators) { for (const item of operators) {
if (!RULE_PROCESSORS[item.type]) { if (!RULE_PROCESSORS[item.type]) {
console.error(`Unknown operator: ${item.type}!`); console.error(`Unknown operator: ${item.type}!`);
continue; continue;
} }
const processor = RULE_PROCESSORS[item.type](item.args); const processor = RULE_PROCESSORS[item.type](item.args);
$.info(`Applying "${item.type}" with arguments: \n >>> ${JSON.stringify(item.args) || 'None'}`); $.info(
rules = ApplyProcessor(processor, rules); `Applying "${item.type}" with arguments: \n >>> ${
} JSON.stringify(item.args) || 'None'
return rules; }`,
} );
rules = ApplyProcessor(processor, rules);
}
return rules;
}
function produce(rules, targetPlatform) { function produce(rules, targetPlatform) {
const producer = RULE_PRODUCERS[targetPlatform]; const producer = RULE_PRODUCERS[targetPlatform];
if (!producer) { if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`); throw new Error(
} `Target platform: ${targetPlatform} is not supported!`,
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { );
return rules }
.map((rule) => { if (
try { typeof producer.type === 'undefined' ||
return producer.func(rule); producer.type === 'SINGLE'
} catch (err) { ) {
console.log(`ERROR: cannot produce rule: ${JSON.stringify(rule)}\nReason: ${err}`); return rules
return ''; .map((rule) => {
} try {
}) return producer.func(rule);
.filter((line) => line.length > 0) } catch (err) {
.join('\n'); console.log(
} else if (producer.type === 'ALL') { `ERROR: cannot produce rule: ${JSON.stringify(
return producer.func(rules); rule,
} )}\nReason: ${err}`,
} );
return '';
}
})
.filter((line) => line.length > 0)
.join('\n');
} else if (producer.type === 'ALL') {
return producer.func(rules);
}
}
return { parse, process, produce }; return { parse, process, produce };
})(); })();
module.exports = {
RuleUtils
};

View File

@ -1,355 +1,413 @@
const $ = require('../core/app'); import { ProxyUtils } from '../core/proxy-utils';
import { RuleUtils } from '../core/rule-utils';
import download from '../utils/download';
import Gist from '../utils/gist';
import $ from '../core/app';
const download = require('../utils/download'); import {
const Gist = require('../utils/gist'); SUBS_KEY,
ARTIFACTS_KEY,
const { ProxyUtils } = require('../core/proxy-utils'); ARTIFACT_REPOSITORY_KEY,
const { RuleUtils } = require('../core/rule-utils'); COLLECTIONS_KEY,
const { SUBS_KEY, ARTIFACTS_KEY, ARTIFACT_REPOSITORY_KEY, COLLECTIONS_KEY, RULES_KEY, SETTINGS_KEY } = require('./constants'); RULES_KEY,
SETTINGS_KEY,
} from './constants';
function register($app) { function register($app) {
// Initialization // Initialization
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY); if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
// RESTful APIs // RESTful APIs
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact); $app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
$app.route('/api/artifact/:name').get(getArtifact).patch(updateArtifact).delete(deleteArtifact); $app.route('/api/artifact/:name')
.get(getArtifact)
.patch(updateArtifact)
.delete(deleteArtifact);
// sync all artifacts // sync all artifacts
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts); $app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
} }
async function getArtifact(req, res) { async function getArtifact(req, res) {
const name = req.params.name; const name = req.params.name;
const action = req.query.action; const action = req.query.action;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = allArtifacts[name]; const artifact = allArtifacts[name];
if (artifact) { if (artifact) {
if (action) { if (action) {
let item; let item;
switch (artifact.type) { switch (artifact.type) {
case 'subscription': case 'subscription':
item = $.read(SUBS_KEY)[artifact.source]; item = $.read(SUBS_KEY)[artifact.source];
break; break;
case 'collection': case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source]; item = $.read(COLLECTIONS_KEY)[artifact.source];
break; break;
case 'rule': case 'rule':
item = $.read(RULES_KEY)[artifact.source]; item = $.read(RULES_KEY)[artifact.source];
break; break;
} }
const output = await produceArtifact({ const output = await produceArtifact({
type: artifact.type, type: artifact.type,
item, item,
platform: artifact.platform platform: artifact.platform,
}); });
if (action === 'preview') { if (action === 'preview') {
res.send(output); res.send(output);
} else if (action === 'sync') { } else if (action === 'sync') {
$.info(`正在上传配置:${artifact.name}\n>>>`); $.info(`正在上传配置:${artifact.name}\n>>>`);
console.log(JSON.stringify(artifact, null, 2)); console.log(JSON.stringify(artifact, null, 2));
try { try {
const resp = await syncArtifact({ const resp = await syncArtifact({
[artifact.name]: { content: output } [artifact.name]: { content: output },
}); });
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
artifact.url = body.files[artifact.name].raw_url.replace(/\/raw\/[^\/]*\/(.*)/, '/raw/$1'); artifact.url = body.files[artifact.name].raw_url.replace(
$.write(allArtifacts, ARTIFACTS_KEY); /\/raw\/[^\/]*\/(.*)/,
res.json({ '/raw/$1',
status: 'success' );
}); $.write(allArtifacts, ARTIFACTS_KEY);
} catch (err) { res.json({
res.status(500).json({ status: 'success',
status: 'failed', });
message: err } catch (err) {
}); res.status(500).json({
} status: 'failed',
} message: err,
} else { });
res.json({ }
status: 'success', }
data: artifact } else {
}); res.json({
} status: 'success',
} else { data: artifact,
res.status(404).json({ });
status: 'failed', }
message: '未找到对应的配置!' } else {
}); res.status(404).json({
} status: 'failed',
message: '未找到对应的配置!',
});
}
} }
function createArtifact(req, res) { function createArtifact(req, res) {
const artifact = req.body; const artifact = req.body;
$.info(`正在创建远程配置:${artifact.name}`); $.info(`正在创建远程配置:${artifact.name}`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
if (allArtifacts[artifact.name]) { if (allArtifacts[artifact.name]) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `远程配置${artifact.name}已存在!` message: `远程配置${artifact.name}已存在!`,
}); });
} else { } else {
if (/^[\w-_.]*$/.test(artifact.name)) { if (/^[\w-_.]*$/.test(artifact.name)) {
allArtifacts[artifact.name] = artifact; allArtifacts[artifact.name] = artifact;
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.status(201).json({ res.status(201).json({
status: 'success', status: 'success',
data: artifact data: artifact,
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。` message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
}); });
} }
} }
} }
function updateArtifact(req, res) { function updateArtifact(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const oldName = req.params.name; const oldName = req.params.name;
const artifact = allArtifacts[oldName]; const artifact = allArtifacts[oldName];
if (artifact) { if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`); $.info(`正在更新远程配置:${artifact.name}`);
const newArtifact = req.body; const newArtifact = req.body;
if (typeof newArtifact.name !== 'undefined' && !/^[\w-_.]*$/.test(newArtifact.name)) { if (
res.status(500).json({ typeof newArtifact.name !== 'undefined' &&
status: 'failed', !/^[\w-_.]*$/.test(newArtifact.name)
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。` ) {
}); res.status(500).json({
} else { status: 'failed',
const merged = { message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
...artifact, });
...newArtifact } else {
}; const merged = {
allArtifacts[merged.name] = merged; ...artifact,
if (merged.name !== oldName) delete allArtifacts[oldName]; ...newArtifact,
$.write(allArtifacts, ARTIFACTS_KEY); };
res.json({ allArtifacts[merged.name] = merged;
status: 'success', if (merged.name !== oldName) delete allArtifacts[oldName];
data: merged $.write(allArtifacts, ARTIFACTS_KEY);
}); res.json({
} status: 'success',
} else { data: merged,
res.status(404).json({ });
status: 'failed', }
message: '未找到对应的远程配置!' } else {
}); res.status(404).json({
} status: 'failed',
message: '未找到对应的远程配置!',
});
}
} }
async function cronSyncArtifacts(_, res) { async function cronSyncArtifacts(_, res) {
$.info('开始同步所有远程配置...'); $.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {}; const files = {};
try { try {
await Promise.all( await Promise.all(
Object.values(allArtifacts).map(async (artifact) => { Object.values(allArtifacts).map(async (artifact) => {
if (artifact.sync) { if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`); $.info(`正在同步云配置:${artifact.name}...`);
let item; let item;
switch (artifact.type) { switch (artifact.type) {
case 'subscription': case 'subscription':
item = $.read(SUBS_KEY)[artifact.source]; item = $.read(SUBS_KEY)[artifact.source];
break; break;
case 'collection': case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source]; item = $.read(COLLECTIONS_KEY)[artifact.source];
break; break;
case 'rule': case 'rule':
item = $.read(RULES_KEY)[artifact.source]; item = $.read(RULES_KEY)[artifact.source];
break; break;
} }
const output = await produceArtifact({ const output = await produceArtifact({
type: artifact.type, type: artifact.type,
item, item,
platform: artifact.platform platform: artifact.platform,
}); });
files[artifact.name] = { files[artifact.name] = {
content: output content: output,
}; };
} }
}) }),
); );
const resp = await syncArtifact(files); const resp = await syncArtifact(files);
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
for (const artifact of Object.values(allArtifacts)) { for (const artifact of Object.values(allArtifacts)) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(/\/raw\/[^\/]*\/(.*)/, '/raw/$1'); artifact.url = body.files[artifact.name].raw_url.replace(
} /\/raw\/[^\/]*\/(.*)/,
'/raw/$1',
);
}
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!'); $.info('全部订阅同步成功!');
res.status(200).end(); res.status(200).end();
} catch (err) { } catch (err) {
res.status(500).json({ res.status(500).json({
error: err error: err,
}); });
$.info(`同步订阅失败,原因:${err}`); $.info(`同步订阅失败,原因:${err}`);
} }
} }
async function deleteArtifact(req, res) { async function deleteArtifact(req, res) {
const name = req.params.name; const name = req.params.name;
$.info(`正在删除远程配置:${name}`); $.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
try { try {
const artifact = allArtifacts[name]; const artifact = allArtifacts[name];
if (!artifact) throw new Error(`远程配置:${name}不存在!`); if (!artifact) throw new Error(`远程配置:${name}不存在!`);
if (artifact.updated) { if (artifact.updated) {
// delete gist // delete gist
await syncArtifact({ await syncArtifact({
filename: name, filename: name,
content: '' content: '',
}); });
} }
// delete local cache // delete local cache
delete allArtifacts[name]; delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.json({ res.json({
status: 'success' status: 'success',
}); });
} catch (err) { } catch (err) {
// delete local cache // delete local cache
delete allArtifacts[name]; delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `无法删除远程配置:${name}, 原因:${err}` message: `无法删除远程配置:${name}, 原因:${err}`,
}); });
} }
} }
function getAllArtifacts(req, res) { function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({ res.json({
status: 'success', status: 'success',
data: allArtifacts data: allArtifacts,
}); });
} }
async function syncArtifact(files) { async function syncArtifact(files) {
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
return Promise.reject('未设置Gist Token'); return Promise.reject('未设置Gist Token');
} }
const manager = new Gist({ const manager = new Gist({
token: gistToken, token: gistToken,
key: ARTIFACT_REPOSITORY_KEY key: ARTIFACT_REPOSITORY_KEY,
}); });
return manager.upload(files); return manager.upload(files);
} }
async function produceArtifact( async function produceArtifact(
{ type, item, platform, noProcessor } = { { type, item, platform, noProcessor } = {
platform: 'JSON', platform: 'JSON',
noProcessor: false noProcessor: false,
} },
) { ) {
if (type === 'subscription') { if (type === 'subscription') {
const sub = item; const sub = item;
const raw = await download(sub.url, sub.ua); const raw = await download(sub.url, sub.ua);
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
if (!noProcessor) { if (!noProcessor) {
// apply processors // apply processors
proxies = await ProxyUtils.process(proxies, sub.process || [], platform); proxies = await ProxyUtils.process(
} proxies,
// check duplicate sub.process || [],
const exist = {}; platform,
for (const proxy of proxies) { );
if (exist[proxy.name]) { }
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '⚠️ 订阅包含重复节点!', '请仔细检测配置!', { // check duplicate
'media-url': const exist = {};
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png' for (const proxy of proxies) {
}); if (exist[proxy.name]) {
break; $.notify(
} '🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
exist[proxy.name] = true; '⚠️ 订阅包含重复节点!',
} '请仔细检测配置!',
// produce {
return ProxyUtils.produce(proxies, platform); 'media-url':
} else if (type === 'collection') { 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
const allSubs = $.read(SUBS_KEY); },
const collection = item; );
const subs = collection['subscriptions']; break;
const results = {}; }
let processed = 0; exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const collection = item;
const subs = collection['subscriptions'];
const results = {};
let processed = 0;
await Promise.all( await Promise.all(
subs.map(async (name) => { subs.map(async (name) => {
const sub = allSubs[name]; const sub = allSubs[name];
try { try {
$.info(`正在处理子订阅:${sub.name}...`); $.info(`正在处理子订阅:${sub.name}...`);
const raw = await download(sub.url, sub.ua); const raw = await download(sub.url, sub.ua);
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = ProxyUtils.parse(raw);
if (!noProcessor) { if (!noProcessor) {
// apply processors // apply processors
currentProxies = await ProxyUtils.process(currentProxies, sub.process || [], platform); currentProxies = await ProxyUtils.process(
} currentProxies,
results[name] = currentProxies; sub.process || [],
processed++; platform,
$.info(`✅ 子订阅:${sub.name}加载成功,进度--${100 * (processed / subs.length).toFixed(1)}% `); );
} catch (err) { }
processed++; results[name] = currentProxies;
$.error( processed++;
`❌ 处理组合订阅中的子订阅: ${sub.name}时出现错误:${err},该订阅已被跳过!进度--${100 * $.info(
(processed / subs.length).toFixed(1)}%` `✅ 子订阅:${sub.name}加载成功,进度--${
); 100 * (processed / subs.length).toFixed(1)
} }% `,
}) );
); } catch (err) {
processed++;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误${err}该订阅已被跳过进度--${
100 * (processed / subs.length).toFixed(1)
}%`,
);
}
}),
);
// merge proxies with the original order // merge proxies with the original order
let proxies = Array.prototype.concat.apply([], subs.map(name => results[name])); let proxies = Array.prototype.concat.apply(
[],
subs.map((name) => results[name]),
);
if (!noProcessor) { if (!noProcessor) {
// apply own processors // apply own processors
proxies = await ProxyUtils.process(proxies, collection.process || [], platform); proxies = await ProxyUtils.process(
} proxies,
if (proxies.length === 0) { collection.process || [],
throw new Error(`组合订阅中不含有效节点!`); platform,
} );
// check duplicate }
const exist = {}; if (proxies.length === 0) {
for (const proxy of proxies) { throw new Error(`组合订阅中不含有效节点!`);
if (exist[proxy.name]) { }
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '⚠️ 订阅包含重复节点!', '请仔细检测配置!', { // check duplicate
'media-url': const exist = {};
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png' for (const proxy of proxies) {
}); if (exist[proxy.name]) {
break; $.notify(
} '🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
exist[proxy.name] = true; '⚠️ 订阅包含重复节点!',
} '请仔细检测配置!',
return ProxyUtils.produce(proxies, platform); {
} else if (type === 'rule') { 'media-url':
const rule = item; 'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
let rules = []; },
for (let i = 0; i < rule.urls.length; i++) { );
const url = rule.urls[i]; break;
$.info(`正在处理URL${url},进度--${100 * ((i + 1) / rule.urls.length).toFixed(1)}% `); }
try { exist[proxy.name] = true;
const { body } = await download(url); }
const currentRules = RuleUtils.parse(body); return ProxyUtils.produce(proxies, platform);
rules = rules.concat(currentRules); } else if (type === 'rule') {
} catch (err) { const rule = item;
$.error(`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`); let rules = [];
} for (let i = 0; i < rule.urls.length; i++) {
} const url = rule.urls[i];
// remove duplicates $.info(
rules = await RuleUtils.process(rules, [ { type: 'Remove Duplicate Filter' } ]); `正在处理URL${url},进度--${
// produce output 100 * ((i + 1) / rule.urls.length).toFixed(1)
return RuleUtils.produce(rules, platform); }% `,
} );
try {
const { body } = await download(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);
}
} }
module.exports = { register, produceArtifact }; export { register, produceArtifact };

View File

@ -1,155 +1,166 @@
const $ = require('../core/app'); import { getPlatformFromHeaders, getFlowHeaders } from './subscriptions';
const { SUBS_KEY, COLLECTIONS_KEY } = require('./constants'); import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
const { getPlatformFromHeaders, getFlowHeaders } = require('./subscriptions'); import { produceArtifact } from './artifacts';
const { produceArtifact } = require('./artifacts'); import $ from '../core/app';
function register($app) { export function register($app) {
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY); if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
$app.get("/download/collection/:name", downloadCollection); $app.get('/download/collection/:name', downloadCollection);
$app.route('/api/collection/:name').get(getCollection).patch(updateCollection).delete(deleteCollection); $app.route('/api/collection/:name')
.get(getCollection)
.patch(updateCollection)
.delete(deleteCollection);
$app.route('/api/collections').get(getAllCollections).post(createCollection); $app.route('/api/collections')
.get(getAllCollections)
.post(createCollection);
} }
// collection API // collection API
async function downloadCollection(req, res) { async function downloadCollection(req, res) {
const { name } = req.params; const { name } = req.params;
const { raw } = req.query || 'false'; const { raw } = req.query || 'false';
const platform = req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCollections = $.read(COLLECTIONS_KEY); const allCollections = $.read(COLLECTIONS_KEY);
const collection = allCollections[name]; const collection = allCollections[name];
$.info(`正在下载组合订阅:${name}`); $.info(`正在下载组合订阅:${name}`);
// forward flow header from the first subscription in this collection // forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const subs = collection['subscriptions']; const subs = collection['subscriptions'];
if (subs.length > 0) { if (subs.length > 0) {
const sub = allSubs[subs[0]]; const sub = allSubs[subs[0]];
const flowInfo = await getFlowHeaders(sub.url); const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set('subscription-userinfo', flowInfo);
} }
} }
if (collection) { if (collection) {
try { try {
const output = await produceArtifact({ const output = await produceArtifact({
type: 'collection', type: 'collection',
item: collection, item: collection,
platform, platform,
noProcessor: raw noProcessor: raw,
}); });
if (platform === 'JSON') { if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(output); res.set('Content-Type', 'application/json;charset=utf-8').send(
} else { output,
res.send(output); );
} } else {
} catch (err) { res.send(output);
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`, `❌ 下载组合订阅错误:${name}`, `🤔 原因:${err}`); }
res.status(500).json({ } catch (err) {
status: 'failed', $.notify(
message: err `🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
}); `❌ 下载组合订阅错误:${name}`,
} `🤔 原因:${err}`,
} else { );
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`, `❌ 未找到组合订阅:${name}`); res.status(500).json({
res.status(404).json({ status: 'failed',
status: 'failed' message: err,
}); });
} }
} else {
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
res.status(404).json({
status: 'failed',
});
}
} }
function createCollection(req, res) { function createCollection(req, res) {
const collection = req.body; const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`); $.info(`正在创建组合订阅:${collection.name}`);
const allCol = $.read(COLLECTIONS_KEY); const allCol = $.read(COLLECTIONS_KEY);
if (allCol[collection.name]) { if (allCol[collection.name]) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅集${collection.name}已存在!` message: `订阅集${collection.name}已存在!`,
}); });
} }
// validate name // validate name
if (/^[\w-_]*$/.test(collection.name)) { if (/^[\w-_]*$/.test(collection.name)) {
allCol[collection.name] = collection; allCol[collection.name] = collection;
$.write(allCol, COLLECTIONS_KEY); $.write(allCol, COLLECTIONS_KEY);
res.status(201).json({ res.status(201).json({
status: 'success', status: 'success',
data: collection data: collection,
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。` message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
}); });
} }
} }
function getCollection(req, res) { function getCollection(req, res) {
const { name } = req.params; const { name } = req.params;
const collection = $.read(COLLECTIONS_KEY)[name]; const collection = $.read(COLLECTIONS_KEY)[name];
if (collection) { if (collection) {
res.json({ res.json({
status: 'success', status: 'success',
data: collection data: collection,
}); });
} else { } else {
res.status(404).json({ res.status(404).json({
status: 'failed', status: 'failed',
message: `未找到订阅集:${name}!` message: `未找到订阅集:${name}!`,
}); });
} }
} }
function updateCollection(req, res) { function updateCollection(req, res) {
const { name } = req.params; const { name } = req.params;
let collection = req.body; let collection = req.body;
const allCol = $.read(COLLECTIONS_KEY); const allCol = $.read(COLLECTIONS_KEY);
if (allCol[name]) { if (allCol[name]) {
const newCol = { const newCol = {
...allCol[name], ...allCol[name],
...collection ...collection,
}; };
$.info(`正在更新组合订阅:${name}...`); $.info(`正在更新组合订阅:${name}...`);
// allow users to update collection name // allow users to update collection name
delete allCol[name]; delete allCol[name];
allCol[collection.name || name] = newCol; allCol[collection.name || name] = newCol;
$.write(allCol, COLLECTIONS_KEY); $.write(allCol, COLLECTIONS_KEY);
res.json({ res.json({
status: 'success', status: 'success',
data: newCol data: newCol,
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅集${name}不存在,无法更新!` message: `订阅集${name}不存在,无法更新!`,
}); });
} }
} }
function deleteCollection(req, res) { function deleteCollection(req, res) {
const { name } = req.params; const { name } = req.params;
$.info(`正在删除组合订阅:${name}`); $.info(`正在删除组合订阅:${name}`);
let allCol = $.read(COLLECTIONS_KEY); let allCol = $.read(COLLECTIONS_KEY);
delete allCol[name]; delete allCol[name];
$.write(allCol, COLLECTIONS_KEY); $.write(allCol, COLLECTIONS_KEY);
res.json({ res.json({
status: 'success' status: 'success',
}); });
} }
function getAllCollections(req, res) { function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
res.json({ res.json({
status: 'success', status: 'success',
data: allCols data: allCols,
}); });
} }
module.exports = {
register
};

View File

@ -1,11 +1,7 @@
module.exports = { export const SETTINGS_KEY = 'settings';
SETTINGS_KEY: 'settings', export const SUBS_KEY = 'subs';
SUBS_KEY: 'subs', export const COLLECTIONS_KEY = 'collections';
COLLECTIONS_KEY: 'collections', export const ARTIFACTS_KEY = 'artifacts';
RULES_KEY: 'rules', export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
BUILT_IN_KEY: 'builtin', export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
ARTIFACTS_KEY: 'artifacts', export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
GIST_BACKUP_KEY: 'Auto Generated Sub-Store Backup',
GIST_BACKUP_FILE_NAME: 'Sub-Store',
ARTIFACT_REPOSITORY_KEY: 'Sub-Store Artifacts Repository'
};

View File

@ -1,115 +1,120 @@
const $ = require('../core/app'); import {
const { ENV } = require('../utils/open-api'); SETTINGS_KEY,
const { IP_API } = require('../utils/geo'); GIST_BACKUP_KEY,
const Gist = require('../utils/gist'); GIST_BACKUP_FILE_NAME,
const { SETTINGS_KEY, GIST_BACKUP_KEY, GIST_BACKUP_FILE_NAME } = require('./constants'); } from './constants';
import { ENV } from '../utils/open-api';
import express from '../utils/express';
import { IP_API } from '../utils/geo';
import Gist from '../utils/gist';
import $ from '../core/app';
function serve() { export default function serve() {
const express = require('../utils/express'); const $app = express();
const $app = express();
// register routes // register routes
const collections = require('./collections'); const collections = require('./collections');
collections.register($app); collections.register($app);
const subscriptions = require('./subscriptions'); const subscriptions = require('./subscriptions');
subscriptions.register($app); subscriptions.register($app);
const settings = require('./settings'); const settings = require('./settings');
settings.register($app); settings.register($app);
const artifacts = require('./artifacts'); const artifacts = require('./artifacts');
artifacts.register($app); artifacts.register($app);
// utils // utils
$app.get('/api/utils/IP_API/:server', IP_API); // IP-API reverse proxy $app.get('/api/utils/IP_API/:server', IP_API); // IP-API reverse proxy
$app.get('/api/utils/env', getEnv); // get runtime environment $app.get('/api/utils/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions $app.get('/api/utils/backup', gistBackup); // gist backup actions
// Redirect sub.store to vercel webpage // Redirect sub.store to vercel webpage
$app.get('/', async (req, res) => { $app.get('/', async (req, res) => {
// 302 redirect // 302 redirect
res.set('location', 'https://sub-store.vercel.app/').status(302).end(); res.set('location', 'https://sub-store.vercel.app/').status(302).end();
}); });
// handle preflight request for QX // handle preflight request for QX
if (ENV().isQX) { if (ENV().isQX) {
$app.options('/', async (req, res) => { $app.options('/', async (req, res) => {
res.status(200).end(); res.status(200).end();
}); });
} }
$app.all('/', (_, res) => { $app.all('/', (_, res) => {
res.send('Hello from sub-store, made with ❤️ by Peng-YM'); res.send('Hello from sub-store, made with ❤️ by Peng-YM');
}); });
$app.start(); $app.start();
} }
function getEnv(req, res) { function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge } = ENV(); const { isNode, isQX, isLoon, isSurge } = ENV();
let backend = 'Node'; let backend = 'Node';
if (isNode) backend = 'Node'; if (isNode) backend = 'Node';
if (isQX) backend = 'QX'; if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon'; if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge'; if (isSurge) backend = 'Surge';
res.json({ res.json({
backend backend,
}); });
} }
async function gistBackup(req, res) { async function gistBackup(req, res) {
const { action } = req.query; const { action } = req.query;
// read token // read token
const { gistToken } = $.read(SETTINGS_KEY); const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: '未找到Gist备份Token!' message: '未找到Gist备份Token!',
}); });
} else { } else {
const gist = new Gist({ const gist = new Gist({
token: gistToken, token: gistToken,
key: GIST_BACKUP_KEY key: GIST_BACKUP_KEY,
}); });
try { try {
let content; let content;
switch (action) { switch (action) {
case 'upload': case 'upload':
// update syncTime. // update syncTime.
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
settings.syncTime = new Date().getTime(); settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
content = $.read('#sub-store'); content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `); if ($.env.isNode)
$.info(`上传备份中...`); content = JSON.stringify($.cache, null, ` `);
await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content } }); $.info(`上传备份中...`);
break; await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content } });
case 'download': break;
$.info(`还原备份中...`); case 'download':
content = await gist.download(GIST_BACKUP_FILE_NAME); $.info(`还原备份中...`);
// restore settings content = await gist.download(GIST_BACKUP_FILE_NAME);
$.write(content, '#sub-store'); // restore settings
if ($.env.isNode) { $.write(content, '#sub-store');
content = JSON.parse(content); if ($.env.isNode) {
Object.keys(content).forEach((key) => { content = JSON.parse(content);
$.write(content[key], key); Object.keys(content).forEach((key) => {
}); $.write(content[key], key);
} });
break; }
} break;
res.json({ }
status: 'success' res.json({
}); status: 'success',
} catch (err) { });
const msg = `${action === 'upload' ? '上传' : '下载'}备份失败!${err}`; } catch (err) {
$.error(msg); const msg = `${
res.status(500).json({ action === 'upload' ? '上传' : '下载'
status: 'failed', }备份失败${err}`;
message: msg $.error(msg);
}); res.status(500).json({
} status: 'failed',
} message: msg,
});
}
}
} }
module.exports = serve;

View File

@ -1,32 +1,27 @@
const $ = require('../core/app'); import { SETTINGS_KEY } from './constants';
const { SETTINGS_KEY } = require('./constants'); import $ from '../core/app';
function register($app) { export function register($app) {
if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY); if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY);
$app.route('/api/settings').get(getSettings).patch(updateSettings);
$app.route('/api/settings').get(getSettings).patch(updateSettings);
} }
function getSettings(req, res) { function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
res.json(settings); res.json(settings);
} }
function updateSettings(req, res) { function updateSettings(req, res) {
const data = req.body; const data = req.body;
const settings = $.read(SETTINGS_KEY); const settings = $.read(SETTINGS_KEY);
$.write( $.write(
{ {
...settings, ...settings,
...data ...data,
}, },
SETTINGS_KEY SETTINGS_KEY,
); );
res.json({ res.json({
status: 'success' status: 'success',
}); });
} }
module.exports = {
register
};

View File

@ -1,205 +1,216 @@
const $ = require("../core/app"); import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
const { produceArtifact } = require('./artifacts'); import { produceArtifact } from './artifacts';
const { SUBS_KEY, COLLECTIONS_KEY } = require('./constants'); import $ from '../core/app';
function register($app) { export function register($app) {
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY); if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
$app.get('/download/:name', downloadSubscription); $app.get('/download/:name', downloadSubscription);
$app.route('/api/sub/:name').get(getSubscription).patch(updateSubscription).delete(deleteSubscription); $app.route('/api/sub/:name')
.get(getSubscription)
.patch(updateSubscription)
.delete(deleteSubscription);
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription); $app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
} }
// subscriptions API // subscriptions API
async function downloadSubscription(req, res) { async function downloadSubscription(req, res) {
const { name } = req.params; const { name } = req.params;
const { raw } = req.query || 'false'; const { raw } = req.query || 'false';
const platform = req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`); $.info(`正在下载订阅:${name}`);
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = allSubs[name]; const sub = allSubs[name];
if (sub) { if (sub) {
try { try {
const output = await produceArtifact({ const output = await produceArtifact({
type: 'subscription', type: 'subscription',
item: sub, item: sub,
platform, platform,
noProcessor: raw noProcessor: raw,
}); });
// forward flow headers // forward flow headers
const flowInfo = await getFlowHeaders(sub.url); const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set('subscription-userinfo', flowInfo);
} }
if (platform === 'JSON') { if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(output); res.set('Content-Type', 'application/json;charset=utf-8').send(
} else { output,
res.send(output); );
} } else {
} catch (err) { res.send(output);
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 无法下载订阅:${name}`, `🤔 原因:${JSON.stringify(err)}`); }
$.error(JSON.stringify(err)); } catch (err) {
res.status(500).json({ $.notify(
status: 'failed', `🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
message: err `❌ 无法下载订阅:${name}`,
}); `🤔 原因:${JSON.stringify(err)}`,
} );
} else { $.error(JSON.stringify(err));
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}`); res.status(500).json({
res.status(404).json({ status: 'failed',
status: 'failed' message: err,
}); });
} }
} else {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}`);
res.status(404).json({
status: 'failed',
});
}
} }
function createSubscription(req, res) { function createSubscription(req, res) {
const sub = req.body; const sub = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
$.info(`正在创建订阅: ${sub.name}`); $.info(`正在创建订阅: ${sub.name}`);
if (allSubs[sub.name]) { if (allSubs[sub.name]) {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅${sub.name}已存在!` message: `订阅${sub.name}已存在!`,
}); });
} }
// validate name // validate name
if (/^[\w-_]*$/.test(sub.name)) { if (/^[\w-_]*$/.test(sub.name)) {
allSubs[sub.name] = sub; allSubs[sub.name] = sub;
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
res.status(201).json({ res.status(201).json({
status: 'success', status: 'success',
data: sub data: sub,
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。` message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
}); });
} }
} }
function getSubscription(req, res) { function getSubscription(req, res) {
const { name } = req.params; const { name } = req.params;
const sub = $.read(SUBS_KEY)[name]; const sub = $.read(SUBS_KEY)[name];
if (sub) { if (sub) {
res.json({ res.json({
status: 'success', status: 'success',
data: sub data: sub,
}); });
} else { } else {
res.status(404).json({ res.status(404).json({
status: 'failed', status: 'failed',
message: `未找到订阅:${name}!` message: `未找到订阅:${name}!`,
}); });
} }
} }
function updateSubscription(req, res) { function updateSubscription(req, res) {
const { name } = req.params; const { name } = req.params;
let sub = req.body; let sub = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
if (allSubs[name]) { if (allSubs[name]) {
const newSub = { const newSub = {
...allSubs[name], ...allSubs[name],
...sub ...sub,
}; };
$.info(`正在更新订阅: ${name}`); $.info(`正在更新订阅: ${name}`);
// allow users to update the subscription name // allow users to update the subscription name
if (name !== sub.name) { if (name !== sub.name) {
// we need to find out all collections refer to this name // we need to find out all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) { for (const k of Object.keys(allCols)) {
const idx = allCols[k].subscriptions.indexOf(name); const idx = allCols[k].subscriptions.indexOf(name);
if (idx !== -1) { if (idx !== -1) {
allCols[k].subscriptions[idx] = sub.name; allCols[k].subscriptions[idx] = sub.name;
} }
} }
// update subscriptions // update subscriptions
delete allSubs[name]; delete allSubs[name];
allSubs[sub.name] = newSub; allSubs[sub.name] = newSub;
} else { } else {
allSubs[name] = newSub; allSubs[name] = newSub;
} }
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
res.json({ res.json({
status: 'success', status: 'success',
data: newSub data: newSub,
}); });
} else { } else {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `订阅${name}不存在,无法更新!` message: `订阅${name}不存在,无法更新!`,
}); });
} }
} }
function deleteSubscription(req, res) { function deleteSubscription(req, res) {
const { name } = req.params; const { name } = req.params;
$.info(`删除订阅:${name}...`); $.info(`删除订阅:${name}...`);
// delete from subscriptions // delete from subscriptions
let allSubs = $.read(SUBS_KEY); let allSubs = $.read(SUBS_KEY);
delete allSubs[name]; delete allSubs[name];
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
// delete from collections // delete from collections
let allCols = $.read(COLLECTIONS_KEY); let allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) { for (const k of Object.keys(allCols)) {
allCols[k].subscriptions = allCols[k].subscriptions.filter((s) => s !== name); allCols[k].subscriptions = allCols[k].subscriptions.filter(
} (s) => s !== name,
$.write(allCols, COLLECTIONS_KEY); );
res.json({ }
status: 'success' $.write(allCols, COLLECTIONS_KEY);
}); res.json({
status: 'success',
});
} }
function getAllSubscriptions(req, res) { function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
res.json({ res.json({
status: 'success', status: 'success',
data: allSubs data: allSubs,
}); });
} }
async function getFlowHeaders(url) { export async function getFlowHeaders(url) {
const { headers } = await $.http.get({ const { headers } = await $.http.get({
url, url,
headers: { headers: {
'User-Agent': 'Quantumult/1.0.13 (iPhone10,3; iOS 14.0)' 'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
} },
}); });
const subkey = Object.keys(headers).filter((k) => /SUBSCRIPTION-USERINFO/i.test(k))[0]; const subkey = Object.keys(headers).filter((k) =>
return headers[subkey]; /SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
} }
function getPlatformFromHeaders(headers) { export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers); const keys = Object.keys(headers);
let UA = ''; let UA = '';
for (let k of keys) { for (let k of keys) {
if (/USER-AGENT/i.test(k)) { if (/USER-AGENT/i.test(k)) {
UA = headers[k]; UA = headers[k];
break; break;
} }
} }
if (UA.indexOf('Quantumult%20X') !== -1) { if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX'; return 'QX';
} else if (UA.indexOf('Surge') !== -1) { } else if (UA.indexOf('Surge') !== -1) {
return 'Surge'; return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) { } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon'; return 'Loon';
} else if (UA.indexOf('Stash') !== -1 || UA.indexOf('Shadowrocket') !== -1) { } else if (
return 'Clash'; UA.indexOf('Stash') !== -1 ||
} else { UA.indexOf('Shadowrocket') !== -1
return null; ) {
} return 'Clash';
} else {
return null;
}
} }
module.exports = {
register,
getPlatformFromHeaders,
getFlowHeaders
};

View File

@ -12,12 +12,13 @@
*/ */
console.log( console.log(
` `
𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 © 𝑷𝒆𝒏𝒈-𝒀𝑴 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 © 𝑷𝒆𝒏𝒈-𝒀𝑴
` `,
); );
const serve = require('./facade'); import serve from './facade';
serve();
serve();

View File

@ -1,30 +1,29 @@
const { HTTP } = require('./open-api'); import { HTTP } from './open-api';
const cache = new Map(); const cache = new Map();
async function download(url, ua) { export default async function download(url, ua) {
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = ua + url; const id = ua + url;
if (cache.has(id)) { if (cache.has(id)) {
return cache.get(id); return cache.get(id);
} }
const $http = HTTP({ const $http = HTTP({
headers: { headers: {
'User-Agent': ua 'User-Agent': ua,
} },
}); });
const result = new Promise((resolve, reject) => { const result = new Promise((resolve, reject) => {
$http.get(url).then((resp) => { $http.get(url).then((resp) => {
const body = resp.body; const body = resp.body;
if (body.replace(/\s/g, '').length === 0) reject(new Error('订阅内容为空!')); if (body.replace(/\s/g, '').length === 0)
else resolve(body); reject(new Error('订阅内容为空!'));
}); else resolve(body);
}); });
});
cache[id] = result; cache.set(id, result);
return result; return result;
} }
module.exports = download;

View File

@ -1,275 +1,285 @@
const $ = require('../core/app'); import { ENV } from './open-api';
const { ENV } = require('./open-api'); import $ from '../core/app';
function express({ port } = { port: 3000 }) { export default function express({ port } = { port: 3000 }) {
const { isNode } = ENV(); const { isNode } = ENV();
const DEFAULT_HEADERS = { const DEFAULT_HEADERS = {
'Content-Type': 'text/plain;charset=UTF-8', 'Content-Type': 'text/plain;charset=UTF-8',
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE', 'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept' 'Access-Control-Allow-Headers':
}; 'Origin, X-Requested-With, Content-Type, Accept',
};
// node support // node support
if (isNode) { if (isNode) {
const express_ = eval(`require("express")`); const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`); const bodyParser = eval(`require("body-parser")`);
const app = express_(); const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver })); app.use(bodyParser.json({ verify: rawBodySaver }));
app.use(bodyParser.urlencoded({ verify: rawBodySaver, extended: true })); app.use(
app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' })); bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
app.use((_, res, next) => { );
res.set(DEFAULT_HEADERS); app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));
next(); app.use((_, res, next) => {
}); res.set(DEFAULT_HEADERS);
next();
});
// adapter // adapter
app.start = () => { app.start = () => {
app.listen(port, () => { app.listen(port, () => {
$.log(`Express started on port: ${port}`); $.log(`Express started on port: ${port}`);
}); });
}; };
return app; return app;
} }
// route handlers // route handlers
const handlers = []; const handlers = [];
// http methods // http methods
const METHODS_NAMES = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', "HEAD'", 'ALL' ]; const METHODS_NAMES = [
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
'OPTIONS',
"HEAD'",
'ALL',
];
// dispatch url to route // dispatch url to route
const dispatch = (request, start = 0) => { const dispatch = (request, start = 0) => {
let { method, url, headers, body } = request; let { method, url, headers, body } = request;
if (/json/i.test(headers['Content-Type'])) { if (/json/i.test(headers['Content-Type'])) {
body = JSON.parse(body); body = JSON.parse(body);
} }
method = method.toUpperCase(); method = method.toUpperCase();
const { path, query } = extractURL(url); const { path, query } = extractURL(url);
// pattern match // pattern match
let handler = null; let handler = null;
let i; let i;
let longestMatchedPattern = 0; let longestMatchedPattern = 0;
for (i = start; i < handlers.length; i++) { for (i = start; i < handlers.length; i++) {
if (handlers[i].method === 'ALL' || method === handlers[i].method) { if (handlers[i].method === 'ALL' || method === handlers[i].method) {
const { pattern } = handlers[i]; const { pattern } = handlers[i];
if (patternMatched(pattern, path)) { if (patternMatched(pattern, path)) {
if (pattern.split('/').length > longestMatchedPattern) { if (pattern.split('/').length > longestMatchedPattern) {
handler = handlers[i]; handler = handlers[i];
longestMatchedPattern = pattern.split('/').length; longestMatchedPattern = pattern.split('/').length;
} }
} }
} }
} }
if (handler) { if (handler) {
// dispatch to next handler // dispatch to next handler
const next = () => { const next = () => {
dispatch(method, url, i); dispatch(method, url, i);
}; };
const req = { const req = {
method, method,
url, url,
path, path,
query, query,
params: extractPathParams(handler.pattern, path), params: extractPathParams(handler.pattern, path),
headers, headers,
body body,
}; };
const res = Response(); const res = Response();
const cb = handler.callback; const cb = handler.callback;
const errFunc = (err) => { const errFunc = (err) => {
res.status(500).json({ res.status(500).json({
status: 'failed', status: 'failed',
message: `Internal Server Error: ${err}` message: `Internal Server Error: ${err}`,
}); });
}; };
if (cb.constructor.name === 'AsyncFunction') { if (cb.constructor.name === 'AsyncFunction') {
cb(req, res, next).catch(errFunc); cb(req, res, next).catch(errFunc);
} else { } else {
try { try {
cb(req, res, next); cb(req, res, next);
} catch (err) { } catch (err) {
errFunc(err); errFunc(err);
} }
} }
} else { } else {
// no route, return 404 // no route, return 404
const res = Response(); const res = Response();
res.status(404).json({ res.status(404).json({
status: 'failed', status: 'failed',
message: 'ERROR: 404 not found' message: 'ERROR: 404 not found',
}); });
} }
}; };
const app = {}; const app = {};
// attach http methods // attach http methods
METHODS_NAMES.forEach((method) => { METHODS_NAMES.forEach((method) => {
app[method.toLowerCase()] = (pattern, callback) => { app[method.toLowerCase()] = (pattern, callback) => {
// add handler // add handler
handlers.push({ method, pattern, callback }); handlers.push({ method, pattern, callback });
}; };
}); });
// chainable route // chainable route
app.route = (pattern) => { app.route = (pattern) => {
const chainApp = {}; const chainApp = {};
METHODS_NAMES.forEach((method) => { METHODS_NAMES.forEach((method) => {
chainApp[method.toLowerCase()] = (callback) => { chainApp[method.toLowerCase()] = (callback) => {
// add handler // add handler
handlers.push({ method, pattern, callback }); handlers.push({ method, pattern, callback });
return chainApp; return chainApp;
}; };
}); });
return chainApp; return chainApp;
}; };
// start service // start service
app.start = () => { app.start = () => {
dispatch($request); dispatch($request);
}; };
return app; return app;
/************************************************ /************************************************
Utility Functions Utility Functions
*************************************************/ *************************************************/
function rawBodySaver(req, res, buf, encoding) { function rawBodySaver(req, res, buf, encoding) {
if (buf && buf.length) { if (buf && buf.length) {
req.rawBody = buf.toString(encoding || 'utf8'); req.rawBody = buf.toString(encoding || 'utf8');
} }
} }
function Response() { function Response() {
let statusCode = 200; let statusCode = 200;
const { isQX, isLoon, isSurge } = ENV(); const { isQX, isLoon, isSurge } = ENV();
const headers = DEFAULT_HEADERS; const headers = DEFAULT_HEADERS;
const STATUS_CODE_MAP = { const STATUS_CODE_MAP = {
200: 'HTTP/1.1 200 OK', 200: 'HTTP/1.1 200 OK',
201: 'HTTP/1.1 201 Created', 201: 'HTTP/1.1 201 Created',
302: 'HTTP/1.1 302 Found', 302: 'HTTP/1.1 302 Found',
307: 'HTTP/1.1 307 Temporary Redirect', 307: 'HTTP/1.1 307 Temporary Redirect',
308: 'HTTP/1.1 308 Permanent Redirect', 308: 'HTTP/1.1 308 Permanent Redirect',
404: 'HTTP/1.1 404 Not Found', 404: 'HTTP/1.1 404 Not Found',
500: 'HTTP/1.1 500 Internal Server Error' 500: 'HTTP/1.1 500 Internal Server Error',
}; };
return new class { return new (class {
status(code) { status(code) {
statusCode = code; statusCode = code;
return this; return this;
} }
send(body = '') { send(body = '') {
const response = { const response = {
status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode, status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode,
body, body,
headers headers,
}; };
if (isQX) { if (isQX) {
$done(response); $done(response);
} else if (isLoon || isSurge) { } else if (isLoon || isSurge) {
$done({ $done({
response response,
}); });
} }
} }
end() { end() {
this.send(); this.send();
} }
html(data) { html(data) {
this.set('Content-Type', 'text/html;charset=UTF-8'); this.set('Content-Type', 'text/html;charset=UTF-8');
this.send(data); this.send(data);
} }
json(data) { json(data) {
this.set('Content-Type', 'application/json;charset=UTF-8'); this.set('Content-Type', 'application/json;charset=UTF-8');
this.send(JSON.stringify(data)); this.send(JSON.stringify(data));
} }
set(key, val) { set(key, val) {
headers[key] = val; headers[key] = val;
return this; return this;
} }
}(); })();
} }
function patternMatched(pattern, path) { function patternMatched(pattern, path) {
if (pattern instanceof RegExp && pattern.test(path)) { if (pattern instanceof RegExp && pattern.test(path)) {
return true; return true;
} else { } else {
// root pattern, match all // root pattern, match all
if (pattern === '/') return true; if (pattern === '/') return true;
// normal string pattern // normal string pattern
if (pattern.indexOf(':') === -1) { if (pattern.indexOf(':') === -1) {
const spath = path.split('/'); const spath = path.split('/');
const spattern = pattern.split('/'); const spattern = pattern.split('/');
for (let i = 0; i < spattern.length; i++) { for (let i = 0; i < spattern.length; i++) {
if (spath[i] !== spattern[i]) { if (spath[i] !== spattern[i]) {
return false; return false;
} }
} }
return true; return true;
} else if (extractPathParams(pattern, path)) { } else if (extractPathParams(pattern, path)) {
// string pattern with path parameters // string pattern with path parameters
return true; return true;
} }
} }
return false; return false;
} }
function extractURL(url) { function extractURL(url) {
// extract path // extract path
const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || []; const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
const path = match[1] || '/'; const path = match[1] || '/';
// extract query string // extract query string
const split = url.indexOf('?'); const split = url.indexOf('?');
const query = {}; const query = {};
if (split !== -1) { if (split !== -1) {
let hashes = url.slice(url.indexOf('?') + 1).split('&'); let hashes = url.slice(url.indexOf('?') + 1).split('&');
for (let i = 0; i < hashes.length; i++) { for (let i = 0; i < hashes.length; i++) {
hash = hashes[i].split('='); const hash = hashes[i].split('=');
query[hash[0]] = hash[1]; query[hash[0]] = hash[1];
} }
} }
return { return {
path, path,
query query,
}; };
} }
function extractPathParams(pattern, path) { function extractPathParams(pattern, path) {
if (pattern.indexOf(':') === -1) { if (pattern.indexOf(':') === -1) {
return null; return null;
} else { } else {
const params = {}; const params = {};
for (let i = 0, j = 0; i < pattern.length; i++, j++) { for (let i = 0, j = 0; i < pattern.length; i++, j++) {
if (pattern[i] === ':') { if (pattern[i] === ':') {
let key = []; let key = [];
let val = []; let val = [];
while (pattern[++i] !== '/' && i < pattern.length) { while (pattern[++i] !== '/' && i < pattern.length) {
key.push(pattern[i]); key.push(pattern[i]);
} }
while (path[j] !== '/' && j < path.length) { while (path[j] !== '/' && j < path.length) {
val.push(path[j++]); val.push(path[j++]);
} }
params[key.join('')] = val.join(''); params[key.join('')] = val.join('');
} else { } else {
if (pattern[i] !== path[j]) { if (pattern[i] !== path[j]) {
return null; return null;
} }
} }
} }
return params; return params;
} }
} }
} }
module.exports = express;

View File

@ -1,209 +1,319 @@
const { HTTP } = require('./open-api'); import { HTTP } from './open-api';
// get proxy flag according to its name // get proxy flag according to its name
function getFlag(name) { export function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js // flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
const flags = { const flags = {
'🇦🇿': [ '阿塞拜疆' ], '🇦🇿': ['阿塞拜疆'],
'🇦🇹': [ '奥地利', '奧地利', 'Austria', '维也纳' ], '🇦🇹': ['奥地利', '奧地利', 'Austria', '维也纳'],
'🇦🇺': [ 'AU', 'Australia', 'Sydney', '澳大利亚', '澳洲', '墨尔本', '悉尼', '土澳', '京澳', '廣澳', '滬澳', '沪澳', '广澳' ], '🇦🇺': [
'🇧🇪': [ 'BE', '比利時', '比利时' ], 'AU',
'🇧🇬': [ '保加利亚', '保加利亞', 'Bulgaria' ], 'Australia',
'🇧🇭': [ 'BH', '巴林' ], 'Sydney',
'🇧🇩': [ 'BD', '孟加拉' ], '澳大利亚',
'🇵🇰': [ '巴基斯坦' ], '澳洲',
'🇰🇭': [ '柬埔寨' ], '墨尔本',
'🇺🇦': [ '烏克蘭', '乌克兰' ], '悉尼',
'🇭🇷': [ '克罗地亚', 'HR', '克羅地亞' ], '土澳',
'🇨🇦': [ 'Canada', 'CANADA', 'CAN', 'Waterloo', '加拿大', '蒙特利尔', '温哥华', '楓葉', '枫叶', '滑铁卢', '多伦多', 'CA' ], '京澳',
'🇨🇭': [ '瑞士', '苏黎世', 'Switzerland', 'Zurich' ], '廣澳',
'🇳🇬': [ '尼日利亚', 'NG', '尼日利亞' ], '滬澳',
'🇨🇿': [ 'Czechia', '捷克' ], '沪澳',
'🇸🇰': [ '斯洛伐克', 'SK' ], '广澳',
'🇷🇸': [ 'RS', '塞尔维亚' ], ],
'🇲🇩': [ '摩爾多瓦', 'MD', '摩尔多瓦' ], '🇧🇪': ['BE', '比利時', '比利时'],
'🇩🇪': [ 'DE', 'German', 'GERMAN', '德国', '德國', '法兰克福', '京德', '滬德', '廣德', '沪德', '广德', 'Frankfurt' ], '🇧🇬': ['保加利亚', '保加利亞', 'Bulgaria'],
'🇩🇰': [ 'DK', 'DNK', '丹麦', '丹麥' ], '🇧🇭': ['BH', '巴林'],
'🇪🇸': [ 'ES', '西班牙', 'Spain' ], '🇧🇩': ['BD', '孟加拉'],
'🇪🇺': [ 'EU', '欧盟', '欧罗巴' ], '🇵🇰': ['巴基斯坦'],
'🇫🇮': [ 'Finland', '芬兰', '芬蘭', '赫尔辛基' ], '🇰🇭': ['柬埔寨'],
'🇫🇷': [ 'FR', 'France', '法国', '法國', '巴黎' ], '🇺🇦': ['烏克蘭', '乌克兰'],
'🇬🇧': [ 'UK', 'GB', 'England', 'United Kingdom', '英国', '伦敦', '英', 'London' ], '🇭🇷': ['克罗地亚', 'HR', '克羅地亞'],
'🇲🇴': [ 'MO', 'Macao', '澳门', '澳門', 'CTM' ], '🇨🇦': [
'🇰🇿': [ '哈萨克斯坦', '哈萨克' ], 'Canada',
'🇭🇺': [ '匈牙利', 'Hungary' ], 'CANADA',
'🇭🇰': [ 'CAN',
'HK', 'Waterloo',
'Hongkong', '加拿大',
'Hong Kong', '蒙特利尔',
'HongKong', '温哥华',
'HONG KONG', '楓葉',
'香港', '枫叶',
'深港', '滑铁卢',
'沪港', '多伦多',
'呼港', 'CA',
'HKT', ],
'HKBN', '🇨🇭': ['瑞士', '苏黎世', 'Switzerland', 'Zurich'],
'HGC', '🇳🇬': ['尼日利亚', 'NG', '尼日利亞'],
'WTT', '🇨🇿': ['Czechia', '捷克'],
'CMI', '🇸🇰': ['斯洛伐克', 'SK'],
'穗港', '🇷🇸': ['RS', '塞尔维亚'],
'京港', '🇲🇩': ['摩爾多瓦', 'MD', '摩尔多瓦'],
'港' '🇩🇪': [
], 'DE',
'🇮🇩': [ 'Indonesia', '印尼', '印度尼西亚', '雅加达' ], 'German',
'🇮🇪': [ 'Ireland', 'IRELAND', '爱尔兰', '愛爾蘭', '都柏林' ], 'GERMAN',
'🇮🇱': [ 'Israel', '以色列' ], '德国',
'🇮🇳': [ 'India', 'IND', 'INDIA', '印度', '孟买', 'MFumbai' ], '德國',
'🇮🇸': [ 'IS', 'ISL', '冰岛', '冰島' ], '法兰克福',
'🇰🇵': [ 'KP', '朝鲜' ], '京德',
'🇰🇷': [ 'KR', 'Korea', 'KOR', '韩国', '首尔', '韩', '韓', '春川', 'Chuncheon', 'Seoul' ], '滬德',
'🇱🇺': [ '卢森堡' ], '廣德',
'🇱🇻': [ 'Latvia', 'Latvija', '拉脱维亚' ], '沪德',
'🇲🇽': [ 'MEX', 'MX', '墨西哥' ], '广德',
'🇲🇾': [ 'MY', 'Malaysia', 'MALAYSIA', '马来西亚', '大馬', '馬來西亞', '吉隆坡' ], 'Frankfurt',
'🇳🇱': [ 'NL', 'Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹' ], ],
'🇳🇵': [ '尼泊尔' ], '🇩🇰': ['DK', 'DNK', '丹麦', '丹麥'],
'🇵🇭': [ 'PH', 'Philippines', '菲律宾', '菲律賓' ], '🇪🇸': ['ES', '西班牙', 'Spain'],
'🇵🇷': [ 'PR', '波多黎各' ], '🇪🇺': ['EU', '欧盟', '欧罗巴'],
'🇷🇴': [ 'RO', '罗马尼亚' ], '🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],
'🇷🇺': [ '🇫🇷': ['FR', 'France', '法国', '法國', '巴黎'],
'RU', '🇬🇧': [
'Russia', 'UK',
'俄罗斯', 'GB',
'俄国', 'England',
'俄羅斯', 'United Kingdom',
'伯力', '英国',
'莫斯科', '伦敦',
'圣彼得堡', '英',
'西伯利亚', 'London',
'新西伯利亚', ],
'京俄', '🇲🇴': ['MO', 'Macao', '澳门', '澳門', 'CTM'],
'杭俄', '🇰🇿': ['哈萨克斯坦', '哈萨克'],
'廣俄', '🇭🇺': ['匈牙利', 'Hungary'],
'滬俄', '🇭🇰': [
'广俄', 'HK',
'沪俄', 'Hongkong',
'Moscow' 'Hong Kong',
], 'HongKong',
'🇸🇦': [ '沙特' ], 'HONG KONG',
'🇸🇪': [ 'SE', 'Sweden', '瑞典' ], '香港',
'🇲🇹': [ '马耳他' ], '深港',
'🇲🇦': [ 'MA', '摩洛哥' ], '沪港',
'🇸🇬': [ 'SG', 'Singapore', 'SINGAPORE', '新加坡', '狮城', '沪新', '京新', '泉新', '穗新', '深新', '杭新', '广新', '廣新', '滬新' ], '呼港',
'🇹🇭': [ 'TH', 'Thailand', '泰国', '泰國', '曼谷' ], 'HKT',
'🇹🇷': [ 'TR', 'Turkey', '土耳其', '伊斯坦布尔' ], 'HKBN',
'🇹🇼': [ 'TW', 'Taiwan', 'TAIWAN', '台湾', '台北', '台中', '新北', '彰化', 'CHT', '台', 'HINET', 'Taipei' ], 'HGC',
'🇺🇸': [ 'WTT',
'US', 'CMI',
'USA', '穗港',
'America', '京港',
'United States', '港',
'美国', ],
'美', '🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],
'京美', '🇮🇪': ['Ireland', 'IRELAND', '爱尔兰', '愛爾蘭', '都柏林'],
'波特兰', '🇮🇱': ['Israel', '以色列'],
'达拉斯', '🇮🇳': ['India', 'IND', 'INDIA', '印度', '孟买', 'MFumbai'],
'俄勒冈', '🇮🇸': ['IS', 'ISL', '冰岛', '冰島'],
'凤凰城', '🇰🇵': ['KP', '朝鲜'],
'费利蒙', '🇰🇷': [
'硅谷', 'KR',
'矽谷', 'Korea',
'拉斯维加斯', 'KOR',
'洛杉矶', '韩国',
'圣何塞', '首尔',
'圣克拉拉', '韩',
'西雅图', '韓',
'芝加哥', '春川',
'沪美', 'Chuncheon',
'哥伦布', 'Seoul',
'纽约', ],
'Los Angeles', '🇱🇺': ['卢森堡'],
'San Jose', '🇱🇻': ['Latvia', 'Latvija', '拉脱维亚'],
'Sillicon Valley', '🇲🇽': ['MEX', 'MX', '墨西哥'],
'Michigan' '🇲🇾': [
], 'MY',
'🇻🇳': [ 'VN', '越南', '胡志明市' ], 'Malaysia',
'🇻🇪': [ 'VE', '委内瑞拉' ], 'MALAYSIA',
'🇮🇹': [ 'Italy', 'IT', 'Nachash', '意大利', '米兰', '義大利' ], '马来西亚',
'🇿🇦': [ 'South Africa', '南非' ], '大馬',
'🇦🇪': [ 'United Arab Emirates', '阿联酋', '迪拜', 'AE' ], '馬來西亞',
'🇧🇷': [ 'BR', 'Brazil', '巴西', '圣保罗' ], '吉隆坡',
'🇯🇵': [ ],
'JP', '🇳🇱': ['NL', 'Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'Japan', '🇳🇵': ['尼泊尔'],
'JAPAN', '🇵🇭': ['PH', 'Philippines', '菲律宾', '菲律賓'],
'日本', '🇵🇷': ['PR', '波多黎各'],
'东京', '🇷🇴': ['RO', '罗马尼亚'],
'大阪', '🇷🇺': [
'埼玉', 'RU',
'沪日', 'Russia',
'穗日', '俄罗斯',
'川日', '俄国',
'中日', '俄羅斯',
'泉日', '伯力',
'杭日', '莫斯科',
'深日', '圣彼得堡',
'辽日', '西伯利亚',
'广日', '新西伯利亚',
'大坂', '京俄',
'Osaka', '杭俄',
'Tokyo' '廣俄',
], '滬俄',
'🇦🇷': [ 'AR', '阿根廷' ], '广俄',
'🇳🇴': [ 'Norway', '挪威', 'NO' ], '沪俄',
'🇨🇳': [ 'CN', 'China', '回国', '中国', '中國', '江苏', '北京', '上海', '广州', '深圳', '杭州', '徐州', '青岛', '宁波', '镇江', 'back' ], 'Moscow',
'🇵🇱': [ 'PL', 'POL', '波兰', '波蘭' ], ],
'🇨🇱': [ '智利' ], '🇸🇦': ['沙特'],
'🇳🇿': [ '新西蘭', '新西兰' ], '🇸🇪': ['SE', 'Sweden', '瑞典'],
'🇬🇷': [ '希腊', '希臘' ], '🇲🇹': ['马耳他'],
'🇪🇬': [ '埃及' ], '🇲🇦': ['MA', '摩洛哥'],
'🇨🇾': [ 'CY', '塞浦路斯' ], '🇸🇬': [
'🇨🇷': [ 'CR', '哥斯达黎加' ], 'SG',
'🇸🇮': [ 'SI', '斯洛文尼亚' ], 'Singapore',
'🇱🇹': [ 'LT', '立陶宛' ], 'SINGAPORE',
'🇵🇦': [ 'PA', '巴拿马' ], '新加坡',
'🇹🇳': [ 'TN', '突尼斯' ], '狮城',
'🇮🇲': [ '马恩岛', '馬恩島' ], '沪新',
'🇧🇾': [ 'BY', '白俄', '白俄罗斯' ], '京新',
'🇵🇹': [ '葡萄牙' ], '泉新',
'🇰🇪': [ 'KE', '肯尼亚' ], '穗新',
'🇰🇬': [ 'KG', '吉尔吉斯坦' ], '深新',
'🇯🇴': [ 'JO', '约旦' ], '杭新',
'🇺🇾': [ 'UY', '乌拉圭' ], '广新',
'🇲🇳': [ '蒙古' ], '廣新',
'🇮🇷': [ 'IR', '伊朗' ], '滬新',
'🇵🇪': [ '秘鲁', '祕魯' ], ],
'🇨🇴': [ '哥伦比亚' ], '🇹🇭': ['TH', 'Thailand', '泰国', '泰國', '曼谷'],
'🇪🇪': [ '爱沙尼亚' ], '🇹🇷': ['TR', 'Turkey', '土耳其', '伊斯坦布尔'],
'🇪🇨': [ 'EC', '厄瓜多尔' ], '🇹🇼': [
'🇲🇰': [ '马其顿', '馬其頓' ], 'TW',
'🇧🇦': [ '波黑共和国', '波黑' ], 'Taiwan',
'🇬🇪': [ '格魯吉亞', '格鲁吉亚' ], 'TAIWAN',
'🇦🇱': [ '阿爾巴尼亞', '阿尔巴尼亚' ], '台湾',
'🏳️‍🌈': [ '流量', '时间', '应急', '过期', 'Bandwidth', 'expire' ] '台北',
}; '台中',
for (let k of Object.keys(flags)) { '新北',
if (flags[k].some((item) => name.indexOf(item) !== -1)) { '彰化',
return k; 'CHT',
} '台',
} 'HINET',
// no flag found 'Taipei',
const oldFlag = (name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/) || [])[0]; ],
return oldFlag || '🏴‍☠️'; '🇺🇸': [
'US',
'USA',
'America',
'United States',
'美国',
'美',
'京美',
'波特兰',
'达拉斯',
'俄勒冈',
'凤凰城',
'费利蒙',
'硅谷',
'矽谷',
'拉斯维加斯',
'洛杉矶',
'圣何塞',
'圣克拉拉',
'西雅图',
'芝加哥',
'沪美',
'哥伦布',
'纽约',
'Los Angeles',
'San Jose',
'Sillicon Valley',
'Michigan',
],
'🇻🇳': ['VN', '越南', '胡志明市'],
'🇻🇪': ['VE', '委内瑞拉'],
'🇮🇹': ['Italy', 'IT', 'Nachash', '意大利', '米兰', '義大利'],
'🇿🇦': ['South Africa', '南非'],
'🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜', 'AE'],
'🇧🇷': ['BR', 'Brazil', '巴西', '圣保罗'],
'🇯🇵': [
'JP',
'Japan',
'JAPAN',
'日本',
'东京',
'大阪',
'埼玉',
'沪日',
'穗日',
'川日',
'中日',
'泉日',
'杭日',
'深日',
'辽日',
'广日',
'大坂',
'Osaka',
'Tokyo',
],
'🇦🇷': ['AR', '阿根廷'],
'🇳🇴': ['Norway', '挪威', 'NO'],
'🇨🇳': [
'CN',
'China',
'回国',
'中国',
'中國',
'江苏',
'北京',
'上海',
'广州',
'深圳',
'杭州',
'徐州',
'青岛',
'宁波',
'镇江',
'back',
],
'🇵🇱': ['PL', 'POL', '波兰', '波蘭'],
'🇨🇱': ['智利'],
'🇳🇿': ['新西蘭', '新西兰'],
'🇬🇷': ['希腊', '希臘'],
'🇪🇬': ['埃及'],
'🇨🇾': ['CY', '塞浦路斯'],
'🇨🇷': ['CR', '哥斯达黎加'],
'🇸🇮': ['SI', '斯洛文尼亚'],
'🇱🇹': ['LT', '立陶宛'],
'🇵🇦': ['PA', '巴拿马'],
'🇹🇳': ['TN', '突尼斯'],
'🇮🇲': ['马恩岛', '馬恩島'],
'🇧🇾': ['BY', '白俄', '白俄罗斯'],
'🇵🇹': ['葡萄牙'],
'🇰🇪': ['KE', '肯尼亚'],
'🇰🇬': ['KG', '吉尔吉斯坦'],
'🇯🇴': ['JO', '约旦'],
'🇺🇾': ['UY', '乌拉圭'],
'🇲🇳': ['蒙古'],
'🇮🇷': ['IR', '伊朗'],
'🇵🇪': ['秘鲁', '祕魯'],
'🇨🇴': ['哥伦比亚'],
'🇪🇪': ['爱沙尼亚'],
'🇪🇨': ['EC', '厄瓜多尔'],
'🇲🇰': ['马其顿', '馬其頓'],
'🇧🇦': ['波黑共和国', '波黑'],
'🇬🇪': ['格魯吉亞', '格鲁吉亚'],
'🇦🇱': ['阿爾巴尼亞', '阿尔巴尼亚'],
'🏳️‍🌈': ['流量', '时间', '应急', '过期', '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 || '🏴‍☠️';
} }
// util API // util API
async function IP_API(req, res) { export async function IP_API(req, res) {
const server = decodeURIComponent(req.params.server); const server = decodeURIComponent(req.params.server);
const $http = HTTP(); const $http = HTTP();
const result = await $http const result = await $http
.get(`http://ip-api.com/json/${server}?lang=zh-CN`) .get(`http://ip-api.com/json/${server}?lang=zh-CN`)
.then((resp) => JSON.parse(resp.body)); .then((resp) => JSON.parse(resp.body));
res.json(result); res.json(result);
} }
module.exports = {
getFlag,
IP_API
};

View File

@ -1,75 +1,77 @@
const { HTTP } = require('./open-api'); import { HTTP } from './open-api';
/** /**
* Gist backup * Gist backup
*/ */
function Gist({ token, key }) { export default function Gist({ token, key }) {
const http = HTTP({ const http = HTTP({
baseURL: 'https://api.github.com', baseURL: 'https://api.github.com',
headers: { headers: {
Authorization: `token ${token}`, Authorization: `token ${token}`,
'User-Agent': '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' '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: { events: {
onResponse: (resp) => { onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) { if (/^[45]/.test(String(resp.statusCode))) {
return Promise.reject(`ERROR: ${JSON.parse(resp.body).message}`); return Promise.reject(
} else { `ERROR: ${JSON.parse(resp.body).message}`,
return resp; );
} } else {
} return resp;
} }
}); },
},
});
async function locate() { async function locate() {
return http.get('/gists').then((response) => { return http.get('/gists').then((response) => {
const gists = JSON.parse(response.body); const gists = JSON.parse(response.body);
for (let g of gists) { for (let g of gists) {
if (g.description === key) { if (g.description === key) {
return g.id; return g.id;
} }
} }
return -1; return -1;
}); });
} }
this.upload = async function(files) { this.upload = async function (files) {
const id = await locate(); const id = await locate();
if (id === -1) { if (id === -1) {
// create a new gist for backup // create a new gist for backup
return http.post({ return http.post({
url: '/gists', url: '/gists',
body: JSON.stringify({ body: JSON.stringify({
description: key, description: key,
public: false, public: false,
files files,
}) }),
}); });
} else { } else {
// update an existing gist // update an existing gist
return http.patch({ return http.patch({
url: `/gists/${id}`, url: `/gists/${id}`,
body: JSON.stringify({ files }) body: JSON.stringify({ files }),
}); });
} }
}; };
this.download = async function(filename) { this.download = async function (filename) {
const id = await locate(); const id = await locate();
if (id === -1) { if (id === -1) {
return Promise.reject('未找到Gist备份'); return Promise.reject('未找到Gist备份');
} else { } else {
try { try {
const { files } = await http.get(`/gists/${id}`).then((resp) => JSON.parse(resp.body)); const { files } = await http
const url = files[filename].raw_url; .get(`/gists/${id}`)
return await http.get(url).then((resp) => resp.body); .then((resp) => JSON.parse(resp.body));
} catch (err) { const url = files[filename].raw_url;
return Promise.reject(err); return await http.get(url).then((resp) => resp.body);
} } catch (err) {
} return Promise.reject(err);
}; }
}
};
} }
module.exports = Gist;

View File

@ -14,9 +14,4 @@ function FULL(length, bool) {
return [...Array(length).keys()].map(() => bool); return [...Array(length).keys()].map(() => bool);
} }
module.exports = { export { AND, OR, NOT, FULL };
AND,
OR,
NOT,
FULL
}

View File

@ -1,271 +1,318 @@
function ENV() { export function ENV() {
const isQX = typeof $task !== 'undefined'; const isQX = typeof $task !== 'undefined';
const isLoon = typeof $loon !== 'undefined'; const isLoon = typeof $loon !== 'undefined';
const isSurge = typeof $httpClient !== 'undefined' && !isLoon; const isSurge = typeof $httpClient !== 'undefined' && !isLoon;
const isNode = eval(`typeof process !== "undefined"`); const isNode = eval(`typeof process !== "undefined"`);
return { isQX, isLoon, isSurge, isNode }; return { isQX, isLoon, isSurge, isNode };
} }
function HTTP(defaultOptions = { baseURL: '' }) { export function HTTP(defaultOptions = { baseURL: '' }) {
const { isQX, isLoon, isSurge, isNode } = ENV(); const { isQX, isLoon, isSurge, isNode } = ENV();
const methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH' ]; const methods = [
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; '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) { function send(method, options) {
options = typeof options === 'string' ? { url: options } : options; options = typeof options === 'string' ? { url: options } : options;
const baseURL = defaultOptions.baseURL; const baseURL = defaultOptions.baseURL;
if (baseURL && !URL_REGEX.test(options.url || '')) { if (baseURL && !URL_REGEX.test(options.url || '')) {
options.url = baseURL ? baseURL + options.url : options.url; options.url = baseURL ? baseURL + options.url : options.url;
} }
options = { ...defaultOptions, ...options }; options = { ...defaultOptions, ...options };
const timeout = options.timeout; const timeout = options.timeout;
const events = { const events = {
...{ ...{
onRequest: () => {}, onRequest: () => {},
onResponse: (resp) => resp, onResponse: (resp) => resp,
onTimeout: () => {} onTimeout: () => {},
}, },
...options.events ...options.events,
}; };
events.onRequest(method, options); events.onRequest(method, options);
let worker; let worker;
if (isQX) { if (isQX) {
worker = $task.fetch({ worker = $task.fetch({
method, method,
url: options.url, url: options.url,
headers: options.headers, headers: options.headers,
body: options.body body: options.body,
}); });
} else if (isLoon || isSurge || isNode) { } else if (isLoon || isSurge || isNode) {
worker = new Promise((resolve, reject) => { worker = new Promise((resolve, reject) => {
const request = isNode ? eval("require('request')") : $httpClient; const request = isNode
request[method.toLowerCase()](options, (err, response, body) => { ? eval("require('request')")
if (err) reject(err); : $httpClient;
else request[method.toLowerCase()](
resolve({ options,
statusCode: response.status || response.statusCode, (err, response, body) => {
headers: response.headers, if (err) reject(err);
body else
}); resolve({
}); statusCode:
}); response.status || response.statusCode,
} headers: response.headers,
body,
});
},
);
});
}
let timeoutid; let timeoutid;
const timer = timeout const timer = timeout
? new Promise((_, reject) => { ? new Promise((_, reject) => {
timeoutid = setTimeout(() => { timeoutid = setTimeout(() => {
events.onTimeout(); events.onTimeout();
return reject(`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`); return reject(
}, timeout); `${method} URL: ${options.url} exceeds the timeout ${timeout} ms`,
}) );
: null; }, timeout);
})
: null;
return (timer return (
? Promise.race([ timer, worker ]).then((res) => { timer
clearTimeout(timeoutid); ? Promise.race([timer, worker]).then((res) => {
return res; clearTimeout(timeoutid);
}) return res;
: worker).then((resp) => events.onResponse(resp)); })
} : worker
).then((resp) => events.onResponse(resp));
}
const http = {}; const http = {};
methods.forEach((method) => (http[method.toLowerCase()] = (options) => send(method, options))); methods.forEach(
return http; (method) =>
(http[method.toLowerCase()] = (options) => send(method, options)),
);
return http;
} }
function API(name = 'untitled', debug = false) { export function API(name = 'untitled', debug = false) {
const { isQX, isLoon, isSurge, isNode } = ENV(); const { isQX, isLoon, isSurge, isNode } = ENV();
return new class { return new (class {
constructor(name, debug) { constructor(name, debug) {
this.name = name; this.name = name;
this.debug = debug; this.debug = debug;
this.http = HTTP(); this.http = HTTP();
this.env = ENV(); this.env = ENV();
this.node = (() => { this.node = (() => {
if (isNode) { if (isNode) {
const fs = eval("require('fs')"); const fs = eval("require('fs')");
return { return {
fs fs,
}; };
} else { } else {
return null; return null;
} }
})(); })();
this.initCache(); this.initCache();
const delay = (t, v) => const delay = (t, v) =>
new Promise(function(resolve) { new Promise(function (resolve) {
setTimeout(resolve.bind(null, v), t); setTimeout(resolve.bind(null, v), t);
}); });
Promise.prototype.delay = function(t) { Promise.prototype.delay = function (t) {
return this.then(function(v) { return this.then(function (v) {
return delay(t, v); return delay(t, v);
}); });
}; };
} }
// persistence // persistence
// initialize cache // initialize cache
initCache() { initCache() {
if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}'); if (isQX)
if (isLoon || isSurge) this.cache = JSON.parse($persistentStore.read(this.name) || '{}'); this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
if (isLoon || isSurge)
this.cache = JSON.parse(
$persistentStore.read(this.name) || '{}',
);
if (isNode) { if (isNode) {
// create a json for root cache // create a json for root cache
let fpath = 'root.json'; let fpath = 'root.json';
if (!this.node.fs.existsSync(fpath)) { if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'wx' }, (err) => console.log(err)); this.node.fs.writeFileSync(
} fpath,
this.root = {}; JSON.stringify({}),
{ flag: 'wx' },
(err) => console.log(err),
);
}
this.root = {};
// create a json file with the given name if not exists // create a json file with the given name if not exists
fpath = `${this.name}.json`; fpath = `${this.name}.json`;
if (!this.node.fs.existsSync(fpath)) { if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'wx' }, (err) => console.log(err)); this.node.fs.writeFileSync(
this.cache = {}; fpath,
} else { JSON.stringify({}),
this.cache = JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)); { flag: 'wx' },
} (err) => console.log(err),
} );
} this.cache = {};
} else {
this.cache = JSON.parse(
this.node.fs.readFileSync(`${this.name}.json`),
);
}
}
}
// store cache // store cache
persistCache() { persistCache() {
const data = JSON.stringify(this.cache, null, 2); const data = JSON.stringify(this.cache, null, 2);
if (isQX) $prefs.setValueForKey(data, this.name); if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name); if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) { if (isNode) {
this.node.fs.writeFileSync(`${this.name}.json`, data, { flag: 'w' }, (err) => console.log(err)); this.node.fs.writeFileSync(
this.node.fs.writeFileSync('root.json', JSON.stringify(this.root, null, 2), { flag: 'w' }, (err) => `${this.name}.json`,
console.log(err) 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) { write(data, key) {
this.log(`SET ${key}`); this.log(`SET ${key}`);
if (key.indexOf('#') !== -1) { if (key.indexOf('#') !== -1) {
key = key.substr(1); key = key.substr(1);
if (isSurge || isLoon) { if (isSurge || isLoon) {
return $persistentStore.write(data, key); return $persistentStore.write(data, key);
} }
if (isQX) { if (isQX) {
return $prefs.setValueForKey(data, key); return $prefs.setValueForKey(data, key);
} }
if (isNode) { if (isNode) {
this.root[key] = data; this.root[key] = data;
} }
} else { } else {
this.cache[key] = data; this.cache[key] = data;
} }
this.persistCache(); this.persistCache();
} }
read(key) { read(key) {
this.log(`READ ${key}`); this.log(`READ ${key}`);
if (key.indexOf('#') !== -1) { if (key.indexOf('#') !== -1) {
key = key.substr(1); key = key.substr(1);
if (isSurge || isLoon) { if (isSurge || isLoon) {
return $persistentStore.read(key); return $persistentStore.read(key);
} }
if (isQX) { if (isQX) {
return $prefs.valueForKey(key); return $prefs.valueForKey(key);
} }
if (isNode) { if (isNode) {
return this.root[key]; return this.root[key];
} }
} else { } else {
return this.cache[key]; return this.cache[key];
} }
} }
delete(key) { delete(key) {
this.log(`DELETE ${key}`); this.log(`DELETE ${key}`);
if (key.indexOf('#') !== -1) { if (key.indexOf('#') !== -1) {
key = key.substr(1); key = key.substr(1);
if (isSurge || isLoon) { if (isSurge || isLoon) {
return $persistentStore.write(null, key); return $persistentStore.write(null, key);
} }
if (isQX) { if (isQX) {
return $prefs.removeValueForKey(key); return $prefs.removeValueForKey(key);
} }
if (isNode) { if (isNode) {
delete this.root[key]; delete this.root[key];
} }
} else { } else {
delete this.cache[key]; delete this.cache[key];
} }
this.persistCache(); this.persistCache();
} }
// notification // notification
notify(title, subtitle = '', content = '', options = {}) { notify(title, subtitle = '', content = '', options = {}) {
const openURL = options['open-url']; const openURL = options['open-url'];
const mediaURL = options['media-url']; const mediaURL = options['media-url'];
if (isQX) $notify(title, subtitle, content, options); if (isQX) $notify(title, subtitle, content, options);
if (isSurge) { if (isSurge) {
$notification.post(title, subtitle, content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`, { $notification.post(
url: openURL title,
}); subtitle,
} content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`,
if (isLoon) { {
let opts = {}; url: openURL,
if (openURL) opts['openUrl'] = openURL; },
if (mediaURL) opts['mediaUrl'] = mediaURL; );
if (JSON.stringify(opts) === '{}') { }
$notification.post(title, subtitle, content); if (isLoon) {
} else { let opts = {};
$notification.post(title, subtitle, content, opts); if (openURL) opts['openUrl'] = openURL;
} if (mediaURL) opts['mediaUrl'] = mediaURL;
} if (JSON.stringify(opts) === '{}') {
if (isNode) { $notification.post(title, subtitle, content);
const content_ = } else {
content + (openURL ? `\n点击跳转: ${openURL}` : '') + (mediaURL ? `\n多媒体: ${mediaURL}` : ''); $notification.post(title, subtitle, content, opts);
console.log(`${title}\n${subtitle}\n${content_}\n\n`); }
} }
} if (isNode) {
const content_ =
content +
(openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
}
}
// other helper functions // other helper functions
log(msg) { log(msg) {
if (this.debug) console.log(`[${this.name}] LOG: ${msg}`); if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);
} }
info(msg) { info(msg) {
console.log(`[${this.name}] INFO: ${msg}`); console.log(`[${this.name}] INFO: ${msg}`);
} }
error(msg) { error(msg) {
console.log(`[${this.name}] ERROR: ${msg}`); console.log(`[${this.name}] ERROR: ${msg}`);
} }
wait(millisec) { wait(millisec) {
return new Promise((resolve) => setTimeout(resolve, millisec)); return new Promise((resolve) => setTimeout(resolve, millisec));
} }
done(value = {}) { done(value = {}) {
if (isQX || isLoon || isSurge) { if (isQX || isLoon || isSurge) {
$done(value); $done(value);
} else if (isNode) { } else if (isNode) {
if (typeof $context !== 'undefined') { if (typeof $context !== 'undefined') {
$context.headers = value.headers; $context.headers = value.headers;
$context.statusCode = value.statusCode; $context.statusCode = value.statusCode;
$context.body = value.body; $context.body = value.body;
} }
} }
} }
}(name, debug); })(name, debug);
} }
module.exports = {
HTTP,
ENV,
API
};

File diff suppressed because one or more lines are too long