Compare commits

...

25 Commits

Author SHA1 Message Date
QuentinHsu bac04587b8 🐞 fix(subscriptions): negative usage flow 2022-10-23 13:36:01 +08:00
Jasonzza 06d0c14abc fix: sync artifacts issue (#164) 2022-09-11 23:52:51 +08:00
Peng-YM 029900085c fix: cron-sync-artifacts.js path 2022-09-10 11:47:34 +08:00
Peng-YM cca5c18d9c build: Update config files 2022-09-10 00:48:11 +08:00
Peng-YM 3de8a1d5be build: Bump version 2022-09-09 23:25:25 +08:00
Peng-YM 94fbe535b3 build: Update GitHub action 2022-09-09 23:15:28 +08:00
Peng-YM 6ee6a9b5e2 build: Split sub-store.min.js for better performance on iOS devices 2022-09-09 23:11:13 +08:00
Peng-YM 1c29771de9 build: Build minimized JavaScripts files 2022-09-09 21:07:28 +08:00
Peng-YM 5845ea1a66 fix (ip-flag.js): Use batch processing to reduce memory footprint 2022-09-09 21:05:05 +08:00
Peng-YM eb32a09def fix (core): Fix QX vmess-http producer
closes #162
2022-09-09 21:04:08 +08:00
Peng-YM c60aec603f fix (core): Proxy port smaller that 80 is now allowed
close #167
2022-09-09 21:03:01 +08:00
Peng-YM 0072739f01 refactor (core): Reworked Trojan URI parser to support IPV6 nodes 2022-08-13 12:57:49 +08:00
Peng-YM 1bd2f5f643 fix: xchacha20-ietf-poly1305 cipher is now supported
closes #151
2022-08-13 11:22:23 +08:00
Peng-YM 0a31f5d5d7 fix: ip-flag does not work on Loon
The node parameter should not include proxy name
2022-08-12 00:00:11 +08:00
Peng-YM 6484edb5db feat: Add ip-flag script 2022-08-11 22:58:48 +08:00
Peng-YM c073870f24 perf: Add support for sending http requests using specific nodes
Only supported on Loon & Surge
2022-08-11 01:07:16 +08:00
Peng-YM e93332048e fix: Occasional crashed when performing migration 2022-08-10 00:28:46 +08:00
Peng-YM 4dcb9ae79e feat: Include cron-sync-artifact in Stash configuration 2022-08-09 22:42:13 +08:00
Peng-YM 6ea3575101 fix: Rename subscription and collection will break artifacts 2022-08-09 22:28:45 +08:00
Peng-YM 26820ea892 fix: proxy duplicate issue 2022-08-04 20:40:40 +08:00
Peng-YM f64e8ecfe4 fix: Loon shadowsocksr obfs-param incorrect 2022-08-02 09:23:34 +08:00
Peng-YM 77604a3544 perf (core): DomainResolveProcessor now cache results 2022-07-19 21:29:06 +08:00
Peng-YM 8f5d027080 fix (cron-sync-artifact): sync timeout due to missing await 2022-07-19 20:55:18 +08:00
Peng-YM 5244de4dba fix (config): Sub-Store url is incorrect in Surge.sgmodule 2022-07-13 15:14:11 +08:00
Peng-YM 4121ec2970 chore (gh-action): Trigger workflow run only if package.json have been modified 2022-07-13 15:10:30 +08:00
42 changed files with 1313 additions and 592 deletions
+7 -3
View File
@@ -3,11 +3,13 @@ on:
push:
branches:
- master
- dev
paths:
- 'backend/package.json'
pull_request:
branches:
- master
- dev
paths:
- 'backend/package.json'
jobs:
build:
runs-on: ubuntu-latest
@@ -46,5 +48,7 @@ jobs:
tag_name: ${{ steps.tag.outputs.release_tag }}
files: |
./backend/sub-store.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store-0.min.js
./backend/dist/sub-store-1.min.js
./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js
File diff suppressed because one or more lines are too long
+16
View File
File diff suppressed because one or more lines are too long
+16
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+53 -28
View File
@@ -11,12 +11,11 @@ import tap from 'gulp-tap';
import pkg from './package.json';
export function peggy() {
return gulp.src('src/**/*.peg')
.pipe(tap(function (file) {
const filename = path.basename(file.path).split(".")[0] + ".js";
return gulp.src('src/**/*.peg').pipe(
tap(function (file) {
const filename = path.basename(file.path).split('.')[0] + '.js';
const raw = fs.readFileSync(file.path, 'utf8');
const contents =
`import * as peggy from 'peggy';
const contents = `import * as peggy from 'peggy';
const grammars = String.raw\`\n${raw}\n\`;
let parser;
export default function getParser() {
@@ -25,15 +24,17 @@ export default function getParser() {
}
return parser;
}\n`;
return newFile(filename, contents)
.pipe(gulp.dest(path.dirname(file.path)))
}));
return newFile(filename, contents).pipe(
gulp.dest(path.dirname(file.path)),
);
}),
);
}
export function lint() {
return gulp
.src('src/**/*.js')
.pipe(eslint({fix: true}))
.pipe(eslint({ fix: true }))
.pipe(eslint.fix())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
@@ -42,9 +43,14 @@ export function lint() {
export function styles() {
return gulp
.src('src/**/*.js')
.pipe(prettier({
singleQuote: true, trailingComma: 'all', tabWidth: 4, bracketSpacing: true
}))
.pipe(
prettier({
singleQuote: true,
trailingComma: 'all',
tabWidth: 4,
bracketSpacing: true,
}),
)
.pipe(gulp.dest((file) => file.base));
}
@@ -59,13 +65,13 @@ function scripts(src, dest) {
{
paths: [
{
'rootPathPrefix': '@',
'rootPathSuffix': 'src',
}
]
}
]
]
rootPathPrefix: '@',
rootPathSuffix: 'src',
},
],
},
],
],
})
.plugin('tinyify')
.bundle()
@@ -74,20 +80,39 @@ function scripts(src, dest) {
}
function banner(dest) {
return () => gulp
.src(dest)
.pipe(header(fs.readFileSync('./banner', 'utf-8'), {pkg, updated: new Date().toLocaleString('zh-CN')}))
.pipe(gulp.dest((file) => file.base));
return () =>
gulp
.src(dest)
.pipe(
header(fs.readFileSync('./banner', 'utf-8'), {
pkg,
updated: new Date().toLocaleString('zh-CN'),
}),
)
.pipe(gulp.dest((file) => file.base));
}
const artifacts = [
{src: 'src/main.js', dest: 'sub-store.min.js'},
{src: 'src/products/resource-parser.loon.js', dest: 'dist/sub-store-parser.loon.min.js'},
{src: 'src/products/cron-sync-artifacts.js', dest: 'dist/cron-sync-artifacts.min.js'}
{ src: 'src/main.js', dest: 'sub-store.min.js' },
{
src: 'src/products/resource-parser.loon.js',
dest: 'dist/sub-store-parser.loon.min.js',
},
{
src: 'src/products/cron-sync-artifacts.js',
dest: 'dist/cron-sync-artifacts.min.js',
},
{ src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' },
{ src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' },
];
export const build = gulp.series(gulp.parallel(artifacts.map(artifact => scripts(artifact.src, artifact.dest))), gulp.parallel(artifacts.map(artifact => banner(artifact.dest))));
export const build = gulp.series(
gulp.parallel(
artifacts.map((artifact) => scripts(artifact.src, artifact.dest)),
),
gulp.parallel(artifacts.map((artifact) => banner(artifact.dest))),
);
const all = gulp.series(peggy, lint, styles, build)
const all = gulp.series(peggy, lint, styles, build);
export default all;
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.11.4",
"version": "2.13.5",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -19,6 +19,7 @@
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"request": "^2.88.2",
"requests": "^0.3.0",
"semver": "^7.3.7",
"static-js-yaml": "^1.0.0",
"uuid": "^8.3.2"
+87
View File
@@ -31,6 +31,7 @@ specifiers:
prettier: 2.6.2
prettier-plugin-sort-imports: ^1.6.1
request: ^2.88.2
requests: ^0.3.0
semver: ^7.3.7
static-js-yaml: ^1.0.0
tinyify: ^3.0.0
@@ -43,6 +44,7 @@ dependencies:
js-base64: registry.npmmirror.com/js-base64/3.7.2
lodash: registry.npmmirror.com/lodash/4.17.21
request: registry.npmmirror.com/request/2.88.2
requests: registry.npmmirror.com/requests/0.3.0
semver: registry.npmmirror.com/semver/7.3.7
static-js-yaml: registry.npmmirror.com/static-js-yaml/1.0.0
uuid: registry.npmmirror.com/uuid/8.3.2
@@ -2250,6 +2252,12 @@ packages:
follow-redirects: registry.npmmirror.com/follow-redirects/1.13.0
dev: true
registry.npmmirror.com/axo/0.0.2:
resolution: {integrity: sha512-8CC4Mb+OhK97UEng0PgiqUDNZjzVcWDsV+G2vLYCQn1jEL7y6VqiRVlZlRu+aA/ckSznmNzW6X1I6nj2As/haQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/axo/-/axo-0.0.2.tgz}
name: axo
version: 0.0.2
dev: false
registry.npmmirror.com/babel-plugin-dynamic-import-node/2.3.3:
resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz}
name: babel-plugin-dynamic-import-node
@@ -4296,6 +4304,12 @@ packages:
es5-ext: registry.npmmirror.com/es5-ext/0.10.61
dev: true
registry.npmmirror.com/eventemitter3/4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz}
name: eventemitter3
version: 4.0.7
dev: false
registry.npmmirror.com/events/3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/events/-/events-3.3.0.tgz}
name: events
@@ -4410,6 +4424,12 @@ packages:
name: extend
version: 3.0.2
registry.npmmirror.com/extendible/0.1.1:
resolution: {integrity: sha512-AglckQA0TJV8/ZmhQcNmaaFcFFPXFIoZbfuoQOlGDK7Jh/roWotYzJ7ik1FBBCHBr8n7CgTR8lXXPAN8Rfb7rw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/extendible/-/extendible-0.1.1.tgz}
name: extendible
version: 0.1.1
dev: false
registry.npmmirror.com/extglob/2.0.4:
resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/extglob/-/extglob-2.0.4.tgz}
name: extglob
@@ -4435,6 +4455,12 @@ packages:
engines: {'0': node >=0.6.0}
dev: false
registry.npmmirror.com/failure/1.1.1:
resolution: {integrity: sha512-lzrrk0NUfjVeU3jLmfU01zP5bfg4XVFxHREYGvgJowaCqHLSQtqIGENH/CU+oSs6yfYObdSM7b9UY/3p2VJOSg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/failure/-/failure-1.1.1.tgz}
name: failure
version: 1.1.1
dev: false
registry.npmmirror.com/falafel/2.2.5:
resolution: {integrity: sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/falafel/-/falafel-2.2.5.tgz}
name: falafel
@@ -5219,6 +5245,12 @@ packages:
glogg: registry.npmmirror.com/glogg/1.0.2
dev: true
registry.npmmirror.com/hang/1.0.0:
resolution: {integrity: sha512-vtBz98Bt/Tbm03cZO5Ymc7ZL8ead/jIx9T5Wg/xuz+9BXPAJNJSdGQW63LoaesogUQKTpHyal339hxTaTf/APg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/hang/-/hang-1.0.0.tgz}
name: hang
version: 1.0.0
dev: false
registry.npmmirror.com/har-schema/2.0.0:
resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz}
name: har-schema
@@ -6336,6 +6368,17 @@ packages:
strip-bom: registry.npmmirror.com/strip-bom/2.0.0
dev: true
registry.npmmirror.com/loads/0.0.4:
resolution: {integrity: sha512-XjPzzYIHkuMNqYyvh6AECQAHi682nyKO9TMdMYnaz7QbPDI/KIeSIjRhAlXIbRMPYAgtLUYgPlD3mtKZ4Q8SYA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/loads/-/loads-0.0.4.tgz}
name: loads
version: 0.0.4
dependencies:
failure: registry.npmmirror.com/failure/1.1.1
one-time: registry.npmmirror.com/one-time/0.0.4
xhr-response: registry.npmmirror.com/xhr-response/1.0.1
xhr-status: registry.npmmirror.com/xhr-status/1.0.1
dev: false
registry.npmmirror.com/locate-path/3.0.0:
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/locate-path/-/locate-path-3.0.0.tgz}
name: locate-path
@@ -6876,6 +6919,12 @@ packages:
semver: registry.npmmirror.com/semver/5.7.1
dev: true
registry.npmmirror.com/node-http-xhr/1.3.4:
resolution: {integrity: sha512-0bA08/2RKWxw6pMkOVd3KP+0F5+ifQLMMTDxrCgxlgkoU1N8DhCbCSAYEqpgaVYM2smvbVVewiXjW+8AyoLfxQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/node-http-xhr/-/node-http-xhr-1.3.4.tgz}
name: node-http-xhr
version: 1.3.4
dev: false
registry.npmmirror.com/node-releases/2.0.4:
resolution: {integrity: sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/node-releases/-/node-releases-2.0.4.tgz}
name: node-releases
@@ -7110,6 +7159,12 @@ packages:
wrappy: registry.npmmirror.com/wrappy/1.0.2
dev: true
registry.npmmirror.com/one-time/0.0.4:
resolution: {integrity: sha512-qAMrwuk2xLEutlASoiPiAMW3EN3K96Ka/ilSXYr6qR1zSVXw2j7+yDSqGTC4T9apfLYxM3tLLjKvgPdAUK7kYQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/one-time/-/one-time-0.0.4.tgz}
name: one-time
version: 0.0.4
dev: false
registry.npmmirror.com/optionator/0.8.3:
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/optionator/-/optionator-0.8.3.tgz}
name: optionator
@@ -8057,6 +8112,20 @@ packages:
uuid: registry.npmmirror.com/uuid/3.4.0
dev: false
registry.npmmirror.com/requests/0.3.0:
resolution: {integrity: sha512-1B6nkiHjC1O1cSgFhEwkc+xd8vuj04h7xSmCg5yI8nmhCIKbPkX47od8erQ2pokBt5qxUO7dwP4jplXD6k6ISA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/requests/-/requests-0.3.0.tgz}
name: requests
version: 0.3.0
dependencies:
axo: registry.npmmirror.com/axo/0.0.2
eventemitter3: registry.npmmirror.com/eventemitter3/4.0.7
extendible: registry.npmmirror.com/extendible/0.1.1
hang: registry.npmmirror.com/hang/1.0.0
loads: registry.npmmirror.com/loads/0.0.4
node-http-xhr: registry.npmmirror.com/node-http-xhr/1.3.4
xhr-send: registry.npmmirror.com/xhr-send/1.0.0
dev: false
registry.npmmirror.com/require-directory/2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz}
name: require-directory
@@ -9731,6 +9800,24 @@ packages:
engines: {node: '>=8'}
dev: true
registry.npmmirror.com/xhr-response/1.0.1:
resolution: {integrity: sha512-m2FlVRCl3VqDcpc8UaWZJpwuHpFR2vYeXv6ipXU2Uuu4vNKFYVEFI0emuJN370Fge+JCbiAnS+JJmSoWVmWrjQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/xhr-response/-/xhr-response-1.0.1.tgz}
name: xhr-response
version: 1.0.1
dev: false
registry.npmmirror.com/xhr-send/1.0.0:
resolution: {integrity: sha512-789EG4qW6Z0nPvG74AV3WWQCnBG5HxJXNiBsnEivZ8OpbvVA0amH0+g+MNT99o5kt/XLdRezm5KS1wJzcGJztw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/xhr-send/-/xhr-send-1.0.0.tgz}
name: xhr-send
version: 1.0.0
dev: false
registry.npmmirror.com/xhr-status/1.0.1:
resolution: {integrity: sha512-VF0WSqtmkf56OmF26LCWsWvRb1a+WYGdHDoQnPPCVUQTM8CVUAOBcUDsm7nP7SQcgEEdrvF4DmhEADuXdGieyw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/xhr-status/-/xhr-status-1.0.1.tgz}
name: xhr-status
version: 1.0.1
dev: false
registry.npmmirror.com/xtend/2.1.2:
resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/xtend/-/xtend-2.1.2.tgz}
name: xtend
+1
View File
@@ -50,6 +50,7 @@ function parse(raw) {
lastParser = parser;
success = true;
$.info(`${parser.name} is activated`);
break;
}
}
}
+10 -32
View File
@@ -2,6 +2,8 @@ import { getIfNotBlank, isPresent, isNotBlank, getIfPresent } from '@/utils';
import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx';
import getTrojanURIParser from './peggy/trojan-uri';
import { Base64 } from 'js-base64';
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
@@ -112,8 +114,11 @@ function URI_SSR() {
line = line.split('/?')[1].split('&');
if (line.length > 1) {
for (const item of line) {
const [key, val] = item.split('=');
other_params[key] = val.trim();
let [key, val] = item.split('=');
val = val.trim();
if (val.length > 0) {
other_params[key] = val;
}
}
}
proxy = {
@@ -237,36 +242,9 @@ function URI_Trojan() {
};
const parse = (line) => {
line = line.split('trojan://')[1];
const [server, port] = line.split('@')[1].split('?')[0].split(':');
const name = decodeURIComponent(line.split('#')[1].trim());
let paramArr = line.split('?');
let scert = null;
let params;
if (paramArr.length > 1) {
paramArr = paramArr[1].split('#')[0].split('&');
params = new Map(
paramArr.map((item) => {
return item.split('=');
}),
);
if (
params.get('allowInsecure') === '1' ||
params.get('allowInsecure') === 'true'
) {
scert = true;
}
}
return {
name: name || `[Trojan] ${server}`, // trojan uri may have no server tag!
type: 'trojan',
server,
port,
password: line.split('@')[0],
sni: getIfPresent(params.get('sni')),
'skip-cert-verify': getIfPresent(scert),
};
const parser = getTrojanURIParser();
const proxy = parser.parse(line);
return proxy;
};
return { name, test, parse };
}
@@ -105,7 +105,7 @@ domain = match:[0-9a-zA-z-_.]+ {
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 80 && port <= 65535) {
if (port >= 0 && port <= 65535) {
return port;
}
throw new Error("Invalid port number: " + port);
@@ -114,7 +114,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
username = & {
let j = peg$currPos;
@@ -103,7 +103,7 @@ domain = match:[0-9a-zA-z-_.]+ {
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 80 && port <= 65535) {
if (port >= 0 && port <= 65535) {
return port;
}
throw new Error("Invalid port number: " + port);
@@ -112,7 +112,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
username = & {
let j = peg$currPos;
@@ -130,7 +130,7 @@ ip = & {
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 80 && port <= 65535) {
if (port >= 0 && port <= 65535) {
return port;
}
}
@@ -142,7 +142,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
method = comma "method" equals cipher:cipher {
proxy.cipher = cipher;
};
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
@@ -128,7 +128,7 @@ ip = & {
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 80 && port <= 65535) {
if (port >= 0 && port <= 65535) {
return port;
}
}
@@ -140,7 +140,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
method = comma "method" equals cipher:cipher {
proxy.cipher = cipher;
};
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
@@ -109,7 +109,7 @@ domain = match:[0-9a-zA-z-_.]+ {
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 80 && port <= 65535) {
if (port >= 0 && port <= 65535) {
return port;
}
}
@@ -154,7 +154,7 @@ vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
@@ -107,7 +107,7 @@ domain = match:[0-9a-zA-z-_.]+ {
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 80 && port <= 65535) {
if (port >= 0 && port <= 65535) {
return port;
}
}
@@ -152,7 +152,7 @@ vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
@@ -0,0 +1,115 @@
import * as peggy from 'peggy';
const grammars = String.raw`
// global initializer
{{
function $set(obj, path, value) {
if (Object(obj) !== obj) return obj;
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
path
.slice(0, -1)
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
path[path.length - 1]
] = value;
return obj;
}
function toBool(str) {
if (typeof str === 'undefined' || str === null) return undefined;
return /(TRUE)|1/i.test(str);
}
}}
{
const proxy = {};
const obfs = {};
const $ = {};
const params = {};
}
start = (trojan) {
return proxy
}
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
proxy.type = "trojan";
proxy.password = password;
proxy.server = server;
proxy.port = port;
proxy.name = name;
// name may be empty
if (!proxy.name) {
proxy.name = server + ":" + port;
}
};
password = match:$[^@]+ {
return decodeURIComponent(match);
};
server = ip/domain;
domain = match:[0-9a-zA-z-_.]+ {
const domain = match.join("");
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
return domain;
}
}
ip = & {
const start = peg$currPos;
let end;
let j = start;
while (j < input.length) {
if (input[j] === ",") break;
if (input[j] === ":") end = j;
j++;
}
peg$currPos = end || j;
$.ip = input.substring(start, end).trim();
return true;
} { return $.ip; }
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 0 && port <= 65535) {
return port;
} else {
throw new Error("Invalid port: " + port);
}
}
params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
if (toBool(params["ws"])) {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
}
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
params[key] = value;
}
single = key:$[a-z]i+ {
params[key] = true;
};
name = "#" + match:$.* {
return decodeURIComponent(match);
}
`;
let parser;
export default function getParser() {
if (!parser) {
parser = peggy.generate(grammars);
}
return parser;
}
@@ -0,0 +1,105 @@
// global initializer
{{
function $set(obj, path, value) {
if (Object(obj) !== obj) return obj;
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
path
.slice(0, -1)
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
path[path.length - 1]
] = value;
return obj;
}
function toBool(str) {
if (typeof str === 'undefined' || str === null) return undefined;
return /(TRUE)|1/i.test(str);
}
}}
{
const proxy = {};
const obfs = {};
const $ = {};
const params = {};
}
start = (trojan) {
return proxy
}
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
proxy.type = "trojan";
proxy.password = password;
proxy.server = server;
proxy.port = port;
proxy.name = name;
// name may be empty
if (!proxy.name) {
proxy.name = server + ":" + port;
}
};
password = match:$[^@]+ {
return decodeURIComponent(match);
};
server = ip/domain;
domain = match:[0-9a-zA-z-_.]+ {
const domain = match.join("");
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
return domain;
}
}
ip = & {
const start = peg$currPos;
let end;
let j = start;
while (j < input.length) {
if (input[j] === ",") break;
if (input[j] === ":") end = j;
j++;
}
peg$currPos = end || j;
$.ip = input.substring(start, end).trim();
return true;
} { return $.ip; }
port = digits:[0-9]+ {
const port = parseInt(digits.join(""), 10);
if (port >= 0 && port <= 65535) {
return port;
} else {
throw new Error("Invalid port: " + port);
}
}
params = "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
if (toBool(params["ws"])) {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
}
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
params[key] = value;
}
single = key:$[a-z]i+ {
params[key] = true;
};
name = "#" + match:$.* {
return decodeURIComponent(match);
}
@@ -1,12 +1,15 @@
import resourceCache from '@/utils/resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo';
import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
operator: "AND",
child: [
{
@@ -21,7 +24,7 @@ The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed a
}
]
}
*/
*/
function ConditionalFilter({ rule }) {
return {
@@ -310,6 +313,9 @@ function ScriptOperator(script, targetPlatform, $arguments) {
const DOMAIN_RESOLVERS = {
Google: async function (domain) {
const id = hex_md5(`GOOGLE:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
@@ -326,9 +332,14 @@ const DOMAIN_RESOLVERS = {
if (answers.length === 0) {
throw new Error('No answers');
}
return answers[answers.length - 1].data;
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain) {
const id = hex_md5(`IP-API:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://ip-api.com/json/${encodeURIComponent(
domain,
@@ -338,9 +349,14 @@ const DOMAIN_RESOLVERS = {
if (body['status'] !== 'success') {
throw new Error(`Status is ${body['status']}`);
}
return body.query;
const result = body.query;
resourceCache.set(id, result);
return result;
},
Cloudflare: async function (domain) {
const id = hex_md5(`CLOUDFLARE:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain,
@@ -357,7 +373,9 @@ const DOMAIN_RESOLVERS = {
if (answers.length === 0) {
throw new Error('No answers');
}
return answers[answers.length - 1].data;
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
};
@@ -614,6 +632,7 @@ function createDynamicFunction(name, script, $arguments) {
'$persistentStore',
'$httpClient',
'$notification',
'ProxyUtils',
`${script}\n return ${name}`,
)(
$arguments,
@@ -625,13 +644,15 @@ function createDynamicFunction(name, script, $arguments) {
$httpClient,
// eslint-disable-next-line no-undef
$notification,
ProxyUtils,
);
} else {
return new Function(
'$arguments',
'$substore',
'lodash',
'ProxyUtils',
`${script}\n return ${name}`,
)($arguments, $, lodash);
)($arguments, $, lodash, ProxyUtils);
}
}
@@ -72,7 +72,7 @@ function shadowsocksr(proxy) {
// obfs
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
result.appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param');
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
+4 -1
View File
@@ -206,7 +206,10 @@ function vmess(proxy) {
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
appendIfPresent(`,obfs-uri=${proxy['ws-opts'].path}`, 'ws-opts.path');
appendIfPresent(
`,obfs-uri=${proxy[`${proxy.network}-opts`].path}`,
`${proxy.network}-opts.path`,
);
appendIfPresent(
`,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`,
`${proxy.network}-opts.headers.Host`,
+11 -8
View File
@@ -1,7 +1,8 @@
import { syncToGist, produceArtifact } from '@/restful/artifacts';
import { version } from '../../package.json';
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
import $ from '@/core/app';
import { produceArtifact } from '@/restful/sync';
import { syncToGist } from '@/restful/artifacts';
!(async function () {
const settings = $.read(SETTINGS_KEY);
@@ -12,7 +13,7 @@ import $ from '@/core/app';
if (!artifacts || artifacts.length === 0) return;
const shouldSync = artifacts.some((artifact) => artifact.sync);
if (shouldSync) doSync();
if (shouldSync) await doSync();
})().finally(() => $.done());
async function doSync() {
@@ -50,12 +51,14 @@ async function doSync() {
const body = JSON.parse(resp.body);
for (const artifact of allArtifacts) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
+39
View File
@@ -0,0 +1,39 @@
/**
* 路由拆分 - 本文件只包含不涉及到解析器的 RESTFul API
*/
import { version } from '../../package.json';
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Sub-Store -- v${version}
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
import migrate from '@/utils/migration';
import express from '@/vendor/express';
import $ from '@/core/app';
import registerCollectionRoutes from '@/restful/collections';
import registerSubscriptionRoutes from '@/restful/subscriptions';
import registerArtifactRoutes from '@/restful/artifacts';
import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort';
migrate();
serve();
function serve() {
const $app = express({ substore: $ });
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
registerArtifactRoutes($app);
registerSettingRoutes($app);
registerSortRoutes($app);
registerMiscRoutes($app);
$app.start();
}
+39
View File
@@ -0,0 +1,39 @@
/**
* 路由拆分 - 本文件仅包含使用到解析器的 RESTFul API
*/
import { version } from '../../package.json';
import migrate from '@/utils/migration';
import express from '@/vendor/express';
import $ from '@/core/app';
import registerDownloadRoutes from '@/restful/download';
import registerPreviewRoutes from '@/restful/preview';
import registerSyncRoutes from '@/restful/sync';
import registerNodeInfoRoutes from '@/restful/node-info';
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Sub-Store -- v${version}
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
migrate();
serve();
function serve() {
const $app = express({ substore: $ });
// register routes
registerDownloadRoutes($app);
registerPreviewRoutes($app);
registerSyncRoutes($app);
registerNodeInfoRoutes($app);
$app.options('/', (req, res) => {
res.status(200).end();
});
$app.start();
}
+5 -274
View File
@@ -1,15 +1,8 @@
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';
import {
SUBS_KEY,
ARTIFACTS_KEY,
ARTIFACT_REPOSITORY_KEY,
COLLECTIONS_KEY,
RULES_KEY,
ARTIFACTS_KEY,
SETTINGS_KEY,
} from '@/constants';
import { deleteByName, findByName, updateByName } from '@/utils/database';
@@ -19,6 +12,7 @@ import {
RequestInvalidError,
ResourceNotFoundError,
} from '@/restful/errors';
import Gist from '@/utils/gist';
export default function register($app) {
// Initialization
@@ -31,10 +25,6 @@ export default function register($app) {
.get(getArtifact)
.patch(updateArtifact)
.delete(deleteArtifact);
// sync all artifacts
$app.get('/api/sync/artifacts', syncAllArtifacts);
$app.get('/api/sync/artifact/:name', syncArtifact);
}
function getAllArtifacts(req, res) {
@@ -160,111 +150,8 @@ async function deleteArtifact(req, res) {
}
}
async function syncArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
if (!artifact) {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Artifact ${name} does not exist!`,
),
404,
);
return;
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
try {
const resp = await syncToGist({
[encodeURIComponent(artifact.name)]: {
content: output,
},
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[
encodeURIComponent(artifact.name)
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {
failed(
res,
new InternalServerError(
`FAILED_TO_SYNC_ARTIFACT`,
`Failed to sync artifact ${name}`,
`Reason: ${err}`,
),
);
}
}
async function syncAllArtifacts(_, res) {
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {};
try {
await Promise.all(
allArtifacts.map(async (artifact) => {
if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`);
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
files[artifact.name] = {
content: output,
};
}
}),
);
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
for (const artifact of allArtifacts) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!');
success(res);
} catch (err) {
failed(
res,
new InternalServerError(
`FAILED_TO_SYNC_ARTIFACTS`,
`Failed to sync all artifacts`,
`Reason: ${err}`,
),
);
$.info(`同步订阅失败,原因:${err}`);
}
function validateArtifactName(name) {
return /^[a-zA-Z0-9._-]*$/.test(name);
}
async function syncToGist(files) {
@@ -279,160 +166,4 @@ async function syncToGist(files) {
return manager.upload(files);
}
async function produceArtifact({ type, name, platform }) {
platform = platform || 'JSON';
// produce Clash node format for ShadowRocket
if (platform === 'ShadowRocket') platform = 'Clash';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let proxies = ProxyUtils.parse(raw);
// apply processors
proxies = await ProxyUtils.process(
proxies,
sub.process || [],
platform,
);
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
const subnames = collection.subscriptions;
const results = {};
let processed = 0;
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
$.info(`正在处理子订阅:${sub.name}...`);
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
platform,
);
results[name] = currentProxies;
processed++;
$.info(
`✅ 子订阅:${sub.name}加载成功,进度--${
100 * (processed / subnames.length).toFixed(1)
}% `,
);
} catch (err) {
processed++;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误${err}该订阅已被跳过进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
);
}
}),
);
// merge proxies with the original order
let proxies = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name]),
);
// apply own processors
proxies = await ProxyUtils.process(
proxies,
collection.process || [],
platform,
);
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点!`);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
}
return ProxyUtils.produce(proxies, platform);
} else if (type === 'rule') {
const allRules = $.read(RULES_KEY);
const rule = findByName(allRules, name);
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
$.info(
`正在处理URL${url},进度--${
100 * ((i + 1) / rule.urls.length).toFixed(1)
}% `,
);
try {
const { body } = await 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);
}
}
function validateArtifactName(name) {
return /^[a-zA-Z0-9._-]*$/.test(name);
}
export { syncToGist, produceArtifact };
export { syncToGist };
+16 -1
View File
@@ -1,5 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { COLLECTIONS_KEY } from '@/constants';
import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
@@ -67,6 +67,21 @@ function updateCollection(req, res) {
...collection,
};
$.info(`正在更新组合订阅:${name}...`);
if (name !== newCol.name) {
// update all artifacts referring this collection
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'collection' &&
artifact.source === oldCol.name
) {
artifact.source = newCol.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allCols, name, newCol);
$.write(allCols, COLLECTIONS_KEY);
success(res, newCol);
+1 -1
View File
@@ -2,10 +2,10 @@ import { getPlatformFromHeaders } from '@/utils/platform';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { produceArtifact } from './artifacts';
import $ from '@/core/app';
import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
import { produceArtifact } from '@/restful/sync';
export default function register($app) {
$app.get('/download/collection/:name', downloadCollection);
+7 -198
View File
@@ -1,33 +1,16 @@
import {
SETTINGS_KEY,
GIST_BACKUP_KEY,
GIST_BACKUP_FILE_NAME,
} from '@/constants';
import { version as substoreVersion } from '../../package.json';
import { ENV, HTTP } from '@/vendor/open-api';
import express from '@/vendor/express';
import Gist from '@/utils/gist';
import migrate from '@/utils/migration';
import $ from '@/core/app';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download';
import registerSettingRoutes, {
updateArtifactStore,
updateGitHubAvatar,
} from './settings';
import registerSettingRoutes from './settings';
import registerPreviewRoutes from './preview';
import registerSortingRoutes from './sort';
import { failed, success } from '@/restful/response';
import {
InternalServerError,
NetworkError,
RequestInvalidError,
} from '@/restful/errors';
import resourceCache from '@/utils/resource-cache';
import producer from '@/core/proxy-utils/producers';
import registerMiscRoutes from './miscs';
import registerNodeInfoRoutes from './node-info';
export default function serve() {
const $app = express({ substore: $ });
@@ -40,183 +23,9 @@ export default function serve() {
registerSortingRoutes($app);
registerSettingRoutes($app);
registerArtifactRoutes($app);
// utils
$app.post('/api/utils/node-info', getNodeInfo);
$app.get('/api/utils/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions
$app.get('/api/utils/refresh', refresh);
// Storage management
$app.route('/api/storage')
.get((req, res) => {
res.json($.read('#sub-store'));
})
.post((req, res) => {
const data = req.body;
$.write(JSON.stringify(data), '#sub-store');
res.end();
});
// Redirect sub.store to vercel webpage
$app.get('/', async (req, res) => {
// 302 redirect
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
});
// handle preflight request for QX
if (ENV().isQX) {
$app.options('/', async (req, res) => {
res.status(200).end();
});
}
$app.all('/', (_, res) => {
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
});
registerSyncRoutes($app);
registerNodeInfoRoutes($app);
registerMiscRoutes($app);
$app.start();
}
function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
}
async function refresh(_, res) {
// 1. get GitHub avatar and artifact store
await updateGitHubAvatar();
await updateArtifactStore();
// 2. clear resource cache
resourceCache.revokeAll();
success(res);
}
async function gistBackup(req, res) {
const { action } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
failed(
res,
new RequestInvalidError(
'GIST_TOKEN_NOT_FOUND',
`GitHub Token is required for backup!`,
),
);
} else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
});
try {
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
// perform migration after restoring from gist
migrate();
break;
}
success(res);
} catch (err) {
failed(
res,
new InternalServerError(
'BACKUP_FAILED',
`Failed to ${action} data to gist!`,
`Reason: ${JSON.stringify(err)}`,
),
);
}
}
}
async function getNodeInfo(req, res) {
const proxy = req.body;
const lang = req.query.lang || 'zh-CN';
let shareUrl;
try {
shareUrl = producer.URI.produce(proxy);
} catch (err) {
// do nothing
}
try {
const $http = HTTP();
const info = await $http
.get({
url: `http://ip-api.com/json/${encodeURIComponent(
proxy.server,
)}?lang=${lang}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',
},
})
.then((resp) => {
const data = JSON.parse(resp.body);
if (data.status !== 'success') {
throw new Error(data.message);
}
// remove unnecessary fields
delete data.status;
return data;
});
success(res, {
shareUrl,
info,
});
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_GET_NODE_INFO',
`Failed to get node info`,
`Reason: ${err}`,
),
);
}
}
+144
View File
@@ -0,0 +1,144 @@
import $ from '@/core/app';
import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
import { version as substoreVersion } from '../../package.json';
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache';
import {
GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY,
SETTINGS_KEY,
} from '@/constants';
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist';
import migrate from '@/utils/migration';
export default function register($app) {
// utils
$app.get('/api/utils/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions
$app.get('/api/utils/refresh', refresh);
// Storage management
$app.route('/api/storage')
.get((req, res) => {
res.json($.read('#sub-store'));
})
.post((req, res) => {
const data = req.body;
$.write(JSON.stringify(data), '#sub-store');
res.end();
});
// Redirect sub.store to vercel webpage
$app.get('/', async (req, res) => {
// 302 redirect
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
});
// handle preflight request for QX
if (ENV().isQX) {
$app.options('/', async (req, res) => {
res.status(200).end();
});
}
$app.all('/', (_, res) => {
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
});
}
function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
}
async function refresh(_, res) {
// 1. get GitHub avatar and artifact store
await updateGitHubAvatar();
await updateArtifactStore();
// 2. clear resource cache
resourceCache.revokeAll();
success(res);
}
async function gistBackup(req, res) {
const { action } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
failed(
res,
new RequestInvalidError(
'GIST_TOKEN_NOT_FOUND',
`GitHub Token is required for backup!`,
),
);
} else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
});
try {
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
// perform migration after restoring from gist
migrate();
break;
}
success(res);
} catch (err) {
failed(
res,
new InternalServerError(
'BACKUP_FAILED',
`Failed to ${action} data to gist!`,
`Reason: ${JSON.stringify(err)}`,
),
);
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import producer from '@/core/proxy-utils/producers';
import { HTTP } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
import { NetworkError } from '@/restful/errors';
export default function register($app) {
$app.post('/api/utils/node-info', getNodeInfo);
}
async function getNodeInfo(req, res) {
const proxy = req.body;
const lang = req.query.lang || 'zh-CN';
let shareUrl;
try {
shareUrl = producer.URI.produce(proxy);
} catch (err) {
// do nothing
}
try {
const $http = HTTP();
const info = await $http
.get({
url: `http://ip-api.com/json/${encodeURIComponent(
proxy.server,
)}?lang=${lang}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',
},
})
.then((resp) => {
const data = JSON.parse(resp.body);
if (data.status !== 'success') {
throw new Error(data.message);
}
// remove unnecessary fields
delete data.status;
return data;
});
success(res, {
shareUrl,
info,
});
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_GET_NODE_INFO',
`Failed to get node info`,
`Reason: ${err}`,
),
);
}
}
+1 -1
View File
@@ -92,7 +92,7 @@ async function compareCollection(req, res) {
// merge proxies with the original order
const original = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name]),
subnames.map((name) => results[name] || []),
);
original.forEach((proxy, i) => {
+23 -5
View File
@@ -5,7 +5,7 @@ import {
RequestInvalidError,
} from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY } from '@/constants';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders } from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
@@ -66,8 +66,12 @@ async function getFlowInfo(req, res) {
}
// unit is KB
const upload = Number(flowHeaders.match(/upload=(\d+)/)[1]);
const download = Number(flowHeaders.match(/download=(\d+)/)[1]);
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/)
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/)
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
@@ -137,14 +141,28 @@ function updateSubscription(req, res) {
$.info(`正在更新订阅: ${name}`);
// allow users to update the subscription name
if (name !== sub.name) {
// we need to find out all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY);
// update all collections refer to this name
const allCols = $.read(COLLECTIONS_KEY) || [];
for (const collection of allCols) {
const idx = collection.subscriptions.indexOf(name);
if (idx !== -1) {
collection.subscriptions[idx] = sub.name;
}
}
// update all artifacts referring this subscription
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'subscription' &&
artifact.source == name
) {
artifact.source = sub.name;
}
}
$.write(allCols, COLLECTIONS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY);
+286
View File
@@ -0,0 +1,286 @@
import $ from '@/core/app';
import {
ARTIFACTS_KEY,
COLLECTIONS_KEY,
RULES_KEY,
SUBS_KEY,
} from '@/constants';
import { failed, success } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
import { findByName } from '@/utils/database';
import download from '@/utils/download';
import { ProxyUtils } from '@/core/proxy-utils';
import { RuleUtils } from '@/core/rule-utils';
import { syncToGist } from '@/restful/artifacts';
export default function register($app) {
// Initialization
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
// sync all artifacts
$app.get('/api/sync/artifacts', syncAllArtifacts);
$app.get('/api/sync/artifact/:name', syncArtifact);
}
async function produceArtifact({ type, name, platform }) {
platform = platform || 'JSON';
// produce Clash node format for ShadowRocket
if (platform === 'ShadowRocket') platform = 'Clash';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let proxies = ProxyUtils.parse(raw);
// apply processors
proxies = await ProxyUtils.process(
proxies,
sub.process || [],
platform,
);
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
const subnames = collection.subscriptions;
const results = {};
let processed = 0;
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
$.info(`正在处理子订阅:${sub.name}...`);
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
platform,
);
results[name] = currentProxies;
processed++;
$.info(
`✅ 子订阅:${sub.name}加载成功,进度--${
100 * (processed / subnames.length).toFixed(1)
}% `,
);
} catch (err) {
processed++;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误${err}该订阅已被跳过进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
);
}
}),
);
// merge proxies with the original order
let proxies = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name]),
);
// apply own processors
proxies = await ProxyUtils.process(
proxies,
collection.process || [],
platform,
);
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点!`);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
}
return ProxyUtils.produce(proxies, platform);
} else if (type === 'rule') {
const allRules = $.read(RULES_KEY);
const rule = findByName(allRules, name);
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
$.info(
`正在处理URL${url},进度--${
100 * ((i + 1) / rule.urls.length).toFixed(1)
}% `,
);
try {
const { body } = await 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);
}
}
async function syncAllArtifacts(_, res) {
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {};
try {
await Promise.all(
allArtifacts.map(async (artifact) => {
if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`);
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
files[artifact.name] = {
content: output,
};
}
}),
);
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
for (const artifact of allArtifacts) {
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!');
success(res);
} catch (err) {
failed(
res,
new InternalServerError(
`FAILED_TO_SYNC_ARTIFACTS`,
`Failed to sync all artifacts`,
`Reason: ${err}`,
),
);
$.info(`同步订阅失败,原因:${err}`);
}
}
async function syncArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
if (!artifact) {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Artifact ${name} does not exist!`,
),
404,
);
return;
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
try {
const resp = await syncToGist({
[encodeURIComponent(artifact.name)]: {
content: output,
},
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[
encodeURIComponent(artifact.name)
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {
failed(
res,
new InternalServerError(
`FAILED_TO_SYNC_ARTIFACT`,
`Failed to sync artifact ${name}`,
`Reason: ${err}`,
),
);
}
}
export { produceArtifact };
+3 -2
View File
@@ -81,7 +81,8 @@ function doMigrationV2() {
useless: 'DEFAULT',
},
};
processes.forEach((p) => {
for (const p of processes) {
if (!p.type) continue;
if (p.type === 'Useless Filter') {
quickSettingOperator.args.useless = 'ENABLED';
} else if (p.type === 'Set Property Operator') {
@@ -120,7 +121,7 @@ function doMigrationV2() {
} else {
newProcesses.push(p);
}
});
}
newProcesses.unshift(quickSettingOperator);
item.process = newProcesses;
}
+11
View File
@@ -253,6 +253,17 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
events.onRequest(method, options);
if (options.node) {
// Surge & Loon allow connecting to a server using a specified proxy node
if (isSurge) {
const build = $environment['surge-build'];
if (build && parseInt(build) >= 2407) {
options['policy-descriptor'] = options.node;
delete options.node;
}
}
}
let worker;
if (isQX) {
worker = $task.fetch({
+3 -3
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -9,6 +9,7 @@
hostname=sub.store
[Script]
http-request https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js, requires-body=true, timeout=120, tag=Sub-Store
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
+2 -1
View File
@@ -1,3 +1,4 @@
hostname=sub.store
^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js
^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
+24 -4
View File
@@ -5,12 +5,32 @@ http:
mitm:
- sub.store
script:
- match: ^https?:\/\/sub\.store
name: sub-store
- match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
name: sub-store-1
type: request
require-body: true
timeout: 120
- match: ^https?:\/\/sub\.store
name: sub-store-0
type: request
require-body: true
timeout: 120
cron:
script:
- name: cron-sync-artifacts
cron: "0 0 * * *"
timeout: 120
script-providers:
sub-store:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.min.js
sub-store-0:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
interval: 86400
sub-store-1:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
interval: 86400
cron-sync-artifacts:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
interval: 86400
+3 -2
View File
@@ -4,6 +4,7 @@
hostname=%APPEND% sub.store
[Script]
Sub-Store = type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/,requires-body=true,timeout=120,max-size=131072
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Sync = type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
+174
View File
@@ -0,0 +1,174 @@
const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
const CACHE_EXPIRATION_TIME_MS = 10 * 60 * 1000;
const $ = $substore;
class ResourceCache {
constructor(expires) {
this.expires = expires;
if (!$.read(RESOURCE_CACHE_KEY)) {
$.write('{}', RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
this._cleanup();
}
_cleanup() {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (new Date().getTime() - updated > this.expires) {
$.delete(`#${id}`);
delete this.resourceCache[id];
clear = true;
}
});
if (clear) this._persist();
}
revokeAll() {
Object.keys(this.resourceCache).forEach((id) => {
$.delete(`#${id}`);
});
this.resourceCache = {};
this._persist();
}
_persist() {
$.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
}
get(id) {
const updated = this.resourceCache[id];
if (updated && new Date().getTime() - updated <= this.expires) {
return $.read(`#${id}`);
}
return null;
}
set(id, value) {
this.resourceCache[id] = new Date().getTime();
this._persist();
$.write(value, `#${id}`);
}
}
const resourceCache = new ResourceCache(CACHE_EXPIRATION_TIME_MS);
async function operator(proxies) {
const { isLoon, isSurge } = $substore.env;
let support = false;
if (isLoon) {
support = true;
} else if (isSurge) {
const build = $environment['surge-build'];
if (build && parseInt(build) >= 2407) {
support = true;
}
}
if (support) {
const batches = [];
const BATCH_SIZE = 10;
let i = 0;
while (i < proxies.length) {
const batch = proxies.slice(i, i + BATCH_SIZE);
await Promise.all(batch.map(async proxy => {
try {
// remove the original flag
let proxyName = removeFlag(proxy.name);
// query ip-api
const countryCode = await queryIpApi(proxy);
proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;
proxy.name = proxyName;
} catch (err) {
// TODO:
}
}));
await sleep(1000);
i += BATCH_SIZE;
}
} else {
$.error(`IP Flag only supports Loon and Surge!`);
}
return proxies;
}
const tasks = new Map();
async function queryIpApi(proxy) {
const id = getId(proxy);
if (tasks.has(id)) {
return tasks.get(id);
}
const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0";
const headers = {
"User-Agent": ua
};
const { isLoon } = $substore.env;
const target = isLoon ? "Loon" : "Surge";
const result = new Promise((resolve, reject) => {
const cached = resourceCache.get(id);
if (cached) {
resolve(cached);
}
const url = `http://ip-api.com/json`;
let node = ProxyUtils.produce([proxy], target);
// Loon 需要去掉节点名字
if (isLoon) {
const s = node.indexOf("=");
node = node.substring(s + 1);
}
$.http.get({
url,
headers,
node
}).then(resp => {
const body = resp.body;
const data = JSON.parse(body);
if (data.status === "success") {
resourceCache.set(id, data.countryCode);
resolve(data.countryCode);
} else {
reject(new Error(data.message));
}
}).catch(err => {
console.log(err);
reject(err);
});
});
tasks.set(id, result);
return result;
}
function getId(proxy) {
return MD5(`IP-FLAG-${proxy.server}-${proxy.port}`);
}
function getFlagEmoji(countryCode) {
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String
.fromCodePoint(...codePoints)
.replace(/🇹🇼/g, '🇨🇳');
}
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
.trim();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
var MD5 = function (d) { var r = M(V(Y(X(d), 8 * d.length))); return r.toLowerCase() }; function M(d) { for (var _, m = "0123456789ABCDEF", f = "", r = 0; r < d.length; r++)_ = d.charCodeAt(r), f += m.charAt(_ >>> 4 & 15) + m.charAt(15 & _); return f } function X(d) { for (var _ = Array(d.length >> 2), m = 0; m < _.length; m++)_[m] = 0; for (m = 0; m < 8 * d.length; m += 8)_[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; return _ } function V(d) { for (var _ = "", m = 0; m < 32 * d.length; m += 8)_ += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); return _ } function Y(d, _) { d[_ >> 5] |= 128 << _ % 32, d[14 + (_ + 64 >>> 9 << 4)] = _; for (var m = 1732584193, f = -271733879, r = -1732584194, i = 271733878, n = 0; n < d.length; n += 16) { var h = m, t = f, g = r, e = i; f = md5_ii(f = md5_ii(f = md5_ii(f = md5_ii(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_hh(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_gg(f = md5_ff(f = md5_ff(f = md5_ff(f = md5_ff(f, r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = md5_ff(r, i = md5_ff(i, m = md5_ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = md5_gg(r, i = md5_gg(i, m = md5_gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = md5_hh(r, i = md5_hh(i, m = md5_hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = md5_ii(r, i = md5_ii(i, m = md5_ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = safe_add(m, h), f = safe_add(f, t), r = safe_add(r, g), i = safe_add(i, e) } return Array(m, f, r, i) } function md5_cmn(d, _, m, f, r, i) { return safe_add(bit_rol(safe_add(safe_add(_, d), safe_add(f, i)), r), m) } function md5_ff(d, _, m, f, r, i, n) { return md5_cmn(_ & m | ~_ & f, d, _, r, i, n) } function md5_gg(d, _, m, f, r, i, n) { return md5_cmn(_ & f | m & ~f, d, _, r, i, n) } function md5_hh(d, _, m, f, r, i, n) { return md5_cmn(_ ^ m ^ f, d, _, r, i, n) } function md5_ii(d, _, m, f, r, i, n) { return md5_cmn(m ^ (_ | ~f), d, _, r, i, n) } function safe_add(d, _) { var m = (65535 & d) + (65535 & _); return (d >> 16) + (_ >> 16) + (m >> 16) << 16 | 65535 & m } function bit_rol(d, _) { return d << _ | d >>> 32 - _ }
View File