mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d636e1b94c | ||
|
|
69726cd5c4 | ||
|
|
8918479b9e | ||
|
|
17504ab5aa | ||
|
|
0d8fa91cd5 | ||
|
|
e7dfa1ce38 | ||
|
|
fe937d6ebf | ||
|
|
b7b734f529 | ||
|
|
f5ef6010bc | ||
|
|
0e82a7669d | ||
|
|
6d11ea0fcc | ||
|
|
75f802f607 | ||
|
|
000e90d114 | ||
|
|
c2499f6779 | ||
|
|
bf9210fc5a | ||
|
|
53dd1fd4c5 | ||
|
|
c541b83037 | ||
|
|
3054d5cd5d | ||
|
|
5a645081d1 | ||
|
|
1fc5b764fe | ||
|
|
5f1415d9d4 | ||
|
|
1e3b4a147a | ||
|
|
905a50c0b9 | ||
|
|
89e8a99729 | ||
|
|
ff8573cae7 | ||
|
|
1ae1ec40ca | ||
|
|
53925518b4 | ||
|
|
f3de132d70 | ||
|
|
3e30a35bc4 | ||
|
|
3e5f3eafdd | ||
|
|
9c78b87834 | ||
|
|
ea88cc1794 | ||
|
|
c8b197c0a1 | ||
|
|
69fab11344 | ||
|
|
955c74a77d | ||
|
|
6d51774d36 | ||
|
|
a91f9d7728 | ||
|
|
df366cf8eb | ||
|
|
c547f34f57 | ||
|
|
a4ff32331a | ||
|
|
14648d6401 | ||
|
|
6216217286 | ||
|
|
6a66475154 | ||
|
|
adc95bba60 | ||
|
|
fab3644b86 | ||
|
|
c21ce0be16 | ||
|
|
fa65eb1850 | ||
|
|
79c9b89c5f | ||
|
|
fca508ba8a | ||
|
|
21b531a44d | ||
|
|
4e5b46a43d | ||
|
|
bf81ca4acf | ||
|
|
e7c0b23222 | ||
|
|
40fb0fd7f3 | ||
|
|
b061fca356 | ||
|
|
d3c6c99b0a | ||
|
|
3fbc280e28 | ||
|
|
9e3e4c6e46 | ||
|
|
bc0dd4b175 | ||
|
|
7603fac036 | ||
|
|
9acc161684 | ||
|
|
024582a99d | ||
|
|
1d31a80b9f | ||
|
|
b2d0276836 | ||
|
|
3211fbf357 | ||
|
|
33a17c2d66 | ||
|
|
2c89a0ddbd | ||
|
|
939022e5a3 | ||
|
|
59bca5670d | ||
|
|
07b38cf971 | ||
|
|
28186f596f | ||
|
|
ea31b1d0ec | ||
|
|
77191f9caa | ||
|
|
07a270963e | ||
|
|
f1e1d50a2c | ||
|
|
a65cd1f1c9 | ||
|
|
5b0e2e1ef2 | ||
|
|
b29266ac57 | ||
|
|
336ddd6706 | ||
|
|
25ec219659 | ||
|
|
41d24b131a | ||
|
|
ba78982f41 | ||
|
|
26193301b3 | ||
|
|
0141e48200 | ||
|
|
5ae6687b1f | ||
|
|
ad6d1ab441 | ||
|
|
f5aea14904 | ||
|
|
4f2c95f6ab | ||
|
|
be1e2c9979 | ||
|
|
347b19e30d | ||
|
|
f94a12bf6e | ||
|
|
bd510a9aa9 | ||
|
|
f02af9d643 | ||
|
|
af8e965866 | ||
|
|
4bebcff1d3 | ||
|
|
7b8f6f7e52 | ||
|
|
49c7107d20 | ||
|
|
8bfa4dbbf2 | ||
|
|
5e14d05c30 | ||
|
|
8c5dca71fb | ||
|
|
4973454f58 | ||
|
|
4c6ba2cdc8 | ||
|
|
9cbbd0e86f | ||
|
|
0320a77451 | ||
|
|
afb9296158 | ||
|
|
9b0c15ebc2 | ||
|
|
46738d5947 | ||
|
|
1f505752ae | ||
|
|
0734a3d563 | ||
|
|
497bc264e3 | ||
|
|
feb207b333 | ||
|
|
9ac1112b37 | ||
|
|
96769598ef | ||
|
|
f8ed6a3342 | ||
|
|
99b19c410d | ||
|
|
9e54507bbb | ||
|
|
20afa0ad22 | ||
|
|
c5b6960b35 | ||
|
|
4dd86cb368 | ||
|
|
4a0319e95f | ||
|
|
090d8a978f | ||
|
|
bc9fae6062 | ||
|
|
048344268c | ||
|
|
c5746f6a6b | ||
|
|
5cb226da62 | ||
|
|
d229047744 | ||
|
|
cb21a8e6ec | ||
|
|
537a00e8a9 | ||
|
|
b770578cba | ||
|
|
47a95e5a3d | ||
|
|
e99f13d487 | ||
|
|
fcab8401e0 | ||
|
|
431b1a3c8e | ||
|
|
36d46003d6 | ||
|
|
ff71f12996 | ||
|
|
f7c08e3a56 | ||
|
|
6eea8bb2d0 | ||
|
|
fc90e22a48 | ||
|
|
26d47b019b | ||
|
|
8e49a78f45 | ||
|
|
edee10cee3 | ||
|
|
20d958d74f | ||
|
|
6427f99545 | ||
|
|
7d2ea10206 | ||
|
|
e862235cb8 | ||
|
|
38f1728e42 | ||
|
|
d963be87f8 | ||
|
|
390e4540d2 | ||
|
|
0bd00406f3 | ||
|
|
b9ce4e8f20 | ||
|
|
de15bbf3ea | ||
|
|
5d6bd1415b | ||
|
|
6e9c3ead4c | ||
|
|
b3ccd5743a | ||
|
|
e18c215fe4 | ||
|
|
e4b54b43a1 | ||
|
|
21726bf950 | ||
|
|
f6ca9af00f | ||
|
|
39b79b6ca4 | ||
|
|
999271fa9d | ||
|
|
5de35c7720 | ||
|
|
06d0c14abc | ||
|
|
029900085c | ||
|
|
cca5c18d9c | ||
|
|
3de8a1d5be | ||
|
|
94fbe535b3 | ||
|
|
6ee6a9b5e2 | ||
|
|
1c29771de9 | ||
|
|
5845ea1a66 | ||
|
|
eb32a09def | ||
|
|
c60aec603f | ||
|
|
0072739f01 | ||
|
|
1bd2f5f643 | ||
|
|
0a31f5d5d7 | ||
|
|
6484edb5db | ||
|
|
c073870f24 | ||
|
|
e93332048e | ||
|
|
4dcb9ae79e | ||
|
|
6ea3575101 | ||
|
|
26820ea892 | ||
|
|
f64e8ecfe4 | ||
|
|
77604a3544 | ||
|
|
8f5d027080 | ||
|
|
5244de4dba | ||
|
|
4121ec2970 | ||
|
|
a949c49192 | ||
|
|
9fba3506f0 | ||
|
|
9677c7ebbd | ||
|
|
03149dcadb | ||
|
|
1bfa6ebb2c | ||
|
|
4cd525824e | ||
|
|
813f2b839d | ||
|
|
3d58534dfe | ||
|
|
f7333c0617 | ||
|
|
f7d4b66db6 | ||
|
|
8c844eb23a | ||
|
|
de892aaa2b | ||
|
|
b143476e71 | ||
|
|
2c4e47166d | ||
|
|
6881148021 | ||
|
|
848491c0f8 | ||
|
|
49c8f2e521 |
71
.github/workflows/main.yml
vendored
71
.github/workflows/main.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: update
|
||||
on:
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths:
|
||||
- "backend/package.json"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths:
|
||||
- "backend/package.json"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,22 +17,55 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'master'
|
||||
ref: "master"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "14"
|
||||
node-version: "16"
|
||||
- name: Install dependencies
|
||||
run: cd backend && npm i
|
||||
- name: Test
|
||||
run: cd backend && npm test
|
||||
- name: Build
|
||||
run: cd backend && npm run build
|
||||
- name: Commit changes
|
||||
if: contains(github.event.head_commit.message, '#build')
|
||||
run: |
|
||||
git config user.email github-actions
|
||||
git config user.name github-actions@github.com
|
||||
git add .
|
||||
git commit -m "Build sub-store.min.js" -a
|
||||
git push
|
||||
npm install -g pnpm
|
||||
cd backend && pnpm i
|
||||
- name: Test
|
||||
run: |
|
||||
cd backend
|
||||
pnpm test
|
||||
- name: Build
|
||||
run: |
|
||||
cd backend
|
||||
pnpm run build
|
||||
- name: Bundle
|
||||
run: |
|
||||
cd backend
|
||||
pnpm i -D estrella
|
||||
pnpm run bundle
|
||||
- id: tag
|
||||
name: Generate release tag
|
||||
run: |
|
||||
cd backend
|
||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
|
||||
- name: Prepare release
|
||||
run: |
|
||||
cd backend
|
||||
pnpm i -D conventional-changelog-cli
|
||||
pnpm run changelog
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body_path: ./backend/CHANGELOG.md
|
||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||
files: |
|
||||
./backend/sub-store.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
|
||||
./backend/dist/sub-store.bundle.js
|
||||
- name: Sync to GitLab
|
||||
env:
|
||||
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
|
||||
run: |
|
||||
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -127,4 +127,11 @@ out
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
||||
# Dist files
|
||||
backend/dist/*
|
||||
!backend/dist/.gitkeep
|
||||
backend/sub-store.min.js
|
||||
|
||||
CHANGELOG.md
|
||||
|
||||
45
README.md
45
README.md
@@ -10,8 +10,8 @@
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
||||
</p>
|
||||
|
||||
[](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml)     
|
||||
|
||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||
|
||||
[](https://www.buymeacoffee.com/PengYM)
|
||||
|
||||
Core functionalities:
|
||||
@@ -19,6 +19,8 @@ Core functionalities:
|
||||
1. Conversion among various formats.
|
||||
2. Subscription formatting.
|
||||
3. Collect multiple subscriptions in one URL.
|
||||
|
||||
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
|
||||
|
||||
## 1. Subscription Conversion
|
||||
|
||||
@@ -28,18 +30,27 @@ Core functionalities:
|
||||
- [x] SSR URI
|
||||
- [x] SSD URI
|
||||
- [x] V2RayN URI
|
||||
- [x] Hysteria2 URI
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP)
|
||||
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
||||
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
|
||||
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
|
||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
|
||||
|
||||
### Supported Target Platforms
|
||||
|
||||
- [x] QX
|
||||
- [x] Loon
|
||||
- [x] Surge
|
||||
- [x] Stash & Clash
|
||||
- [x] Stash
|
||||
- [x] Clash.Meta
|
||||
- [x] Clash
|
||||
- [x] ShadowRocket
|
||||
- [x] V2Ray
|
||||
- [x] V2Ray URI
|
||||
- [x] Plain JSON
|
||||
|
||||
## 2. Subscription Formatting
|
||||
|
||||
@@ -61,34 +72,36 @@ Core functionalities:
|
||||
- [x] **Regex rename operator**: replace by regex in proxy names.
|
||||
- [x] **Regex delete operator**: delete by regex in proxy names.
|
||||
- [x] **Script operator**: modify proxy by script.
|
||||
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
|
||||
|
||||
### Development
|
||||
|
||||
Go to `backend` and `web` directories, install node dependencies:
|
||||
Install `pnpm`
|
||||
|
||||
Go to `backend` directories, install node dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
1. In `backend`, run the backend server on http://localhost:3000
|
||||
|
||||
```
|
||||
npm run serve
|
||||
pnpm start
|
||||
```
|
||||
|
||||
2. In`web`, start the vue-cli server
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
This project is under the GPL V3 LICENSE.
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#sub-store-org/sub-store&Date)
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
||||
- Speicial thanks to @Orz-3 and @58xinian for their awesome icons.
|
||||
- Special thanks to @Orz-3 and @58xinian for their awesome icons.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @updated: <%= updated %>
|
||||
* @version: <%= pkg.version %>
|
||||
* @author: Peng-YM
|
||||
* @github: https://github.com/Peng-YM/Sub-Store
|
||||
* @github: https://github.com/sub-store-org/Sub-Store
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
|
||||
|
||||
23
backend/bundle.js
Normal file
23
backend/bundle.js
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
content = content.replace(
|
||||
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
|
||||
'$2',
|
||||
);
|
||||
fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
const { build } = require('estrella');
|
||||
build({
|
||||
entry: 'dist/sub-store.no-bundle.js',
|
||||
outfile: 'dist/sub-store.bundle.js',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
});
|
||||
0
backend/dist/.gitkeep
vendored
Normal file
0
backend/dist/.gitkeep
vendored
Normal file
16
backend/dist/cron-sync-artifacts.min.js
vendored
16
backend/dist/cron-sync-artifacts.min.js
vendored
File diff suppressed because one or more lines are too long
16
backend/dist/sub-store-parser.loon.min.js
vendored
16
backend/dist/sub-store-parser.loon.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sub-store",
|
||||
"version": "2.8.6",
|
||||
"version": "2.14.116",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -8,7 +8,9 @@
|
||||
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
||||
"serve": "node sub-store.min.js",
|
||||
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
|
||||
"build": "gulp"
|
||||
"build": "gulp",
|
||||
"bundle": "node bundle.js",
|
||||
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
|
||||
},
|
||||
"author": "Peng-YM",
|
||||
"license": "GPL-3.0",
|
||||
@@ -19,6 +21,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"
|
||||
@@ -29,7 +32,7 @@
|
||||
"@babel/preset-env": "^7.18.0",
|
||||
"@babel/register": "^7.17.7",
|
||||
"@types/gulp": "^4.0.9",
|
||||
"axios": "^0.20.0",
|
||||
"axios": "^0.21.2",
|
||||
"babel-plugin-relative-path-import": "^2.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"browser-pack-flat": "^3.4.2",
|
||||
@@ -50,4 +53,4 @@
|
||||
"prettier-plugin-sort-imports": "^1.6.1",
|
||||
"tinyify": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
8539
backend/pnpm-lock.yaml
generated
8539
backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,14 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
|
||||
export const SETTINGS_KEY = 'settings';
|
||||
export const SUBS_KEY = 'subs';
|
||||
export const COLLECTIONS_KEY = 'collections';
|
||||
export const FILES_KEY = 'files';
|
||||
export const MODULES_KEY = 'modules';
|
||||
export const ARTIFACTS_KEY = 'artifacts';
|
||||
export const RULES_KEY = 'rules';
|
||||
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
||||
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
||||
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
||||
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
||||
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
|
||||
export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; // cached-script-resource CSR
|
||||
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import download from '@/utils/download';
|
||||
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
import PROXY_PREPROCESSORS from './preprocessors';
|
||||
import PROXY_PRODUCERS from './producers';
|
||||
@@ -36,7 +36,7 @@ function parse(raw) {
|
||||
if (lastParser) {
|
||||
const [proxy, error] = tryParse(lastParser, line);
|
||||
if (!error) {
|
||||
proxies.push(proxy);
|
||||
proxies.push(lastParse(proxy));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,11 @@ function parse(raw) {
|
||||
for (const parser of PROXY_PARSERS) {
|
||||
const [proxy, error] = tryParse(parser, line);
|
||||
if (!error) {
|
||||
proxies.push(proxy);
|
||||
proxies.push(lastParse(proxy));
|
||||
lastParser = parser;
|
||||
success = true;
|
||||
$.info(`${parser.name} is activated`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,35 +63,50 @@ function parse(raw) {
|
||||
return proxies;
|
||||
}
|
||||
|
||||
async function process(proxies, operators = [], targetPlatform) {
|
||||
async function process(proxies, operators = [], targetPlatform, source) {
|
||||
for (const item of operators) {
|
||||
// process script
|
||||
let script;
|
||||
const $arguments = {};
|
||||
let $arguments = {};
|
||||
if (item.type.indexOf('Script') !== -1) {
|
||||
const { mode, content } = item.args;
|
||||
if (mode === 'link') {
|
||||
const url = content;
|
||||
let noCache;
|
||||
let url = content;
|
||||
if (url.endsWith('#noCache')) {
|
||||
url = url.replace(/#noCache$/, '');
|
||||
noCache = true;
|
||||
}
|
||||
// extract link arguments
|
||||
const rawArgs = url.split('#');
|
||||
if (rawArgs.length > 1) {
|
||||
for (const pair of rawArgs[1].split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1] || true;
|
||||
$arguments[key] = value;
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
||||
} catch (e) {
|
||||
for (const pair of rawArgs[1].split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1];
|
||||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||
$arguments[key] =
|
||||
value == null || value === ''
|
||||
? true
|
||||
: decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if this is a remote script, download it
|
||||
try {
|
||||
script = await download(url.split('#')[0]);
|
||||
$.info(`Script loaded: >>>\n ${script}`);
|
||||
script = await download(
|
||||
`${url.split('#')[0]}${noCache ? '#noCache' : ''}`,
|
||||
);
|
||||
// $.info(`Script loaded: >>>\n ${script}`);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
// skip the script if download failed.
|
||||
continue;
|
||||
throw new Error(`无法下载脚本: ${url}`);
|
||||
}
|
||||
} else {
|
||||
script = content;
|
||||
@@ -113,6 +129,7 @@ async function process(proxies, operators = [], targetPlatform) {
|
||||
script,
|
||||
targetPlatform,
|
||||
$arguments,
|
||||
source,
|
||||
);
|
||||
} else {
|
||||
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
||||
@@ -122,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform) {
|
||||
return proxies;
|
||||
}
|
||||
|
||||
function produce(proxies, targetPlatform) {
|
||||
function produce(proxies, targetPlatform, type) {
|
||||
const producer = PROXY_PRODUCERS[targetPlatform];
|
||||
if (!producer) {
|
||||
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
||||
@@ -136,10 +153,21 @@ function produce(proxies, targetPlatform) {
|
||||
|
||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||
let localPort = 10000;
|
||||
return proxies
|
||||
.map((proxy) => {
|
||||
try {
|
||||
return producer.produce(proxy);
|
||||
let line = producer.produce(proxy, type);
|
||||
if (
|
||||
line.length > 0 &&
|
||||
line.includes('__SubStoreLocalPort__')
|
||||
) {
|
||||
line = line.replace(
|
||||
/__SubStoreLocalPort__/g,
|
||||
localPort++,
|
||||
);
|
||||
}
|
||||
return line;
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Cannot produce proxy: ${JSON.stringify(
|
||||
@@ -154,7 +182,7 @@ function produce(proxies, targetPlatform) {
|
||||
.filter((line) => line.length > 0)
|
||||
.join('\n');
|
||||
} else if (producer.type === 'ALL') {
|
||||
return producer.produce(proxies);
|
||||
return producer.produce(proxies, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +190,9 @@ export const ProxyUtils = {
|
||||
parse,
|
||||
process,
|
||||
produce,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isIP,
|
||||
};
|
||||
|
||||
function tryParse(parser, line) {
|
||||
@@ -181,3 +212,73 @@ function safeMatch(parser, line) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function lastParse(proxy) {
|
||||
if (proxy.server) {
|
||||
proxy.server = proxy.server
|
||||
.trim()
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\]$/, '');
|
||||
}
|
||||
if (proxy.type === 'trojan') {
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
}
|
||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
|
||||
if (transporthost && !transportHost) {
|
||||
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
|
||||
delete proxy[`${proxy.network}-opts`].headers.host;
|
||||
}
|
||||
}
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
transportHost = Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost;
|
||||
if (transportHost) {
|
||||
proxy.sni = transportHost;
|
||||
}
|
||||
}
|
||||
if (!proxy.sni && !isIP(proxy.server)) {
|
||||
proxy.sni = proxy.server;
|
||||
}
|
||||
}
|
||||
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
|
||||
if (
|
||||
!proxy.tls &&
|
||||
['ws', 'http'].includes(proxy.network) &&
|
||||
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
|
||||
!isIP(proxy.server)
|
||||
) {
|
||||
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
|
||||
proxy[`${proxy.network}-opts`].headers =
|
||||
proxy[`${proxy.network}-opts`].headers || {};
|
||||
proxy[`${proxy.network}-opts`].headers.Host =
|
||||
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
|
||||
? [proxy.server]
|
||||
: proxy.server;
|
||||
}
|
||||
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
|
||||
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (transportHost && !Array.isArray(transportHost)) {
|
||||
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
|
||||
}
|
||||
if (transportPath && !Array.isArray(transportPath)) {
|
||||
proxy[`${proxy.network}-opts`].path = [transportPath];
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
@@ -21,12 +23,21 @@ function URI_SS() {
|
||||
};
|
||||
content = content.split('#')[0]; // strip proxy name
|
||||
// handle IPV4 and IPV6
|
||||
const serverAndPort = content.match(/@([^/]*)(\/|$)/)[1];
|
||||
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
let userInfoStr = Base64.decode(content.split('@')[0]);
|
||||
if (!serverAndPortArray) {
|
||||
content = Base64.decode(content);
|
||||
userInfoStr = content.split('@')[0];
|
||||
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
|
||||
}
|
||||
const serverAndPort = serverAndPortArray[1];
|
||||
const portIdx = serverAndPort.lastIndexOf(':');
|
||||
proxy.server = serverAndPort.substring(0, portIdx);
|
||||
proxy.port = serverAndPort.substring(portIdx + 1);
|
||||
proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
|
||||
/\d+/,
|
||||
)?.[0];
|
||||
|
||||
const userInfo = Base64.decode(content.split('@')[0]).split(':');
|
||||
const userInfo = userInfoStr.split(':');
|
||||
proxy.cipher = userInfo[0];
|
||||
proxy.password = userInfo[1];
|
||||
|
||||
@@ -112,8 +123,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 = {
|
||||
@@ -145,7 +159,7 @@ function URI_VMess() {
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split('vmess://')[1];
|
||||
const content = Base64.decode(line);
|
||||
let content = Base64.decode(line);
|
||||
if (/=\s*vmess/.test(content)) {
|
||||
// Quantumult VMess URI format
|
||||
const partitions = content.split(',').map((p) => p.trim());
|
||||
@@ -197,30 +211,111 @@ function URI_VMess() {
|
||||
}
|
||||
return proxy;
|
||||
} else {
|
||||
// V2rayN URI format
|
||||
const params = JSON.parse(content);
|
||||
let params = {};
|
||||
|
||||
try {
|
||||
// V2rayN URI format
|
||||
params = JSON.parse(content);
|
||||
} catch (e) {
|
||||
// Shadowrocket URI format
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
|
||||
content = Base64.decode(base64Line);
|
||||
|
||||
for (const addon of qs.split('&')) {
|
||||
const [key, valueRaw] = addon.split('=');
|
||||
let value = valueRaw;
|
||||
value = decodeURIComponent(valueRaw);
|
||||
if (value.indexOf(',') === -1) {
|
||||
params[key] = value;
|
||||
} else {
|
||||
params[key] = value.split(',');
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [___, cipher, uuid, server, port] =
|
||||
/(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
|
||||
|
||||
params.scy = cipher;
|
||||
params.id = uuid;
|
||||
params.port = port;
|
||||
params.add = server;
|
||||
}
|
||||
const proxy = {
|
||||
name: params.ps,
|
||||
name: params.ps ?? params.remarks,
|
||||
type: 'vmess',
|
||||
server: params.add,
|
||||
port: params.port,
|
||||
cipher: 'auto', // V2rayN has no default cipher! use aes-128-gcm as default.
|
||||
port: parseInt(getIfPresent(params.port), 10),
|
||||
cipher: getIfPresent(params.scy, 'auto'),
|
||||
uuid: params.id,
|
||||
alterId: getIfPresent(params.aid, 0),
|
||||
tls: params.tls === 'tls' || params.tls === true,
|
||||
alterId: parseInt(
|
||||
getIfPresent(params.aid ?? params.alterId, 0),
|
||||
10,
|
||||
),
|
||||
tls: ['tls', true, 1, '1'].includes(params.tls),
|
||||
'skip-cert-verify': isPresent(params.verify_cert)
|
||||
? !params.verify_cert
|
||||
: undefined,
|
||||
};
|
||||
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
|
||||
if (proxy.tls && proxy.sni) {
|
||||
proxy.sni = params.sni;
|
||||
}
|
||||
// handle obfs
|
||||
if (params.net === 'ws') {
|
||||
if (params.net === 'ws' || params.obfs === 'websocket') {
|
||||
proxy.network = 'ws';
|
||||
proxy['ws-opts'] = {
|
||||
path: getIfNotBlank(params.path),
|
||||
headers: { Host: getIfNotBlank(params.host) },
|
||||
};
|
||||
if (proxy.tls && params.host) {
|
||||
proxy.sni = params.host;
|
||||
} else if (
|
||||
['tcp', 'http'].includes(params.net) ||
|
||||
params.obfs === 'http'
|
||||
) {
|
||||
proxy.network = 'http';
|
||||
} else if (['grpc'].includes(params.net)) {
|
||||
proxy.network = 'grpc';
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = params.host ?? params.obfsParam;
|
||||
try {
|
||||
const parsedObfs = JSON.parse(transportHost);
|
||||
const parsedHost = parsedObfs?.Host;
|
||||
if (parsedHost) {
|
||||
transportHost = parsedHost;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
let transportPath = params.path;
|
||||
|
||||
if (proxy.network === 'http') {
|
||||
if (transportHost) {
|
||||
transportHost = Array.isArray(transportHost)
|
||||
? transportHost[0]
|
||||
: transportHost;
|
||||
}
|
||||
if (transportPath) {
|
||||
transportPath = Array.isArray(transportPath)
|
||||
? transportPath[0]
|
||||
: transportPath;
|
||||
}
|
||||
}
|
||||
if (transportPath || transportHost) {
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
'grpc-service-name': getIfNotBlank(transportPath),
|
||||
'_grpc-type': getIfNotBlank(params.type),
|
||||
};
|
||||
} else {
|
||||
proxy[`${proxy.network}-opts`] = {
|
||||
path: getIfNotBlank(transportPath),
|
||||
headers: { Host: getIfNotBlank(transportHost) },
|
||||
};
|
||||
}
|
||||
} else {
|
||||
delete proxy.network;
|
||||
}
|
||||
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
|
||||
// sni 优先级应高于 host
|
||||
if (proxy.tls && !proxy.sni && transportHost) {
|
||||
proxy.sni = transportHost;
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
@@ -229,6 +324,139 @@ function URI_VMess() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function URI_VLESS() {
|
||||
const name = 'URI VLESS Parser';
|
||||
const test = (line) => {
|
||||
return /^vless:\/\//.test(line);
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split('vless://')[1];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, uuid, server, port, addons, name] =
|
||||
/^(.*?)@(.*?):(\d+)\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
||||
port = parseInt(`${port}`, 10);
|
||||
uuid = decodeURIComponent(uuid);
|
||||
name = decodeURIComponent(name) ?? `VLESS ${server}:${port}`;
|
||||
const proxy = {
|
||||
type: 'vless',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
};
|
||||
const params = {};
|
||||
for (const addon of addons.split('&')) {
|
||||
const [key, valueRaw] = addon.split('=');
|
||||
let value = valueRaw;
|
||||
value = decodeURIComponent(valueRaw);
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
proxy.tls = params.security && params.security !== 'none';
|
||||
proxy.sni = params.sni;
|
||||
proxy.flow = params.flow;
|
||||
proxy['client-fingerprint'] = params.fp;
|
||||
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
|
||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
|
||||
|
||||
if (['reality'].includes(params.security)) {
|
||||
const opts = {};
|
||||
if (params.pbk) {
|
||||
opts['public-key'] = params.pbk;
|
||||
}
|
||||
if (params.sid) {
|
||||
opts['short-id'] = params.sid;
|
||||
}
|
||||
if (Object.keys(opts).length > 0) {
|
||||
proxy[`${params.security}-opts`] = opts;
|
||||
}
|
||||
}
|
||||
|
||||
proxy.network = params.type;
|
||||
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
|
||||
const opts = {};
|
||||
if (params.path) {
|
||||
opts.path = params.path;
|
||||
}
|
||||
if (params.host) {
|
||||
opts.headers = { Host: params.host };
|
||||
}
|
||||
if (params.serviceName) {
|
||||
opts[`${proxy.network}-service-name`] = params.serviceName;
|
||||
}
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
opts['_grpc-type'] = params.mode || 'gun';
|
||||
}
|
||||
if (Object.keys(opts).length > 0) {
|
||||
proxy[`${proxy.network}-opts`] = opts;
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network === 'ws') {
|
||||
proxy.sni = proxy['ws-opts']?.headers?.Host;
|
||||
} else if (proxy.network === 'http') {
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
|
||||
}
|
||||
}
|
||||
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
function URI_Hysteria2() {
|
||||
const name = 'URI Hysteria2 Parser';
|
||||
const test = (line) => {
|
||||
return /^hysteria2:\/\//.test(line);
|
||||
};
|
||||
const parse = (line) => {
|
||||
line = line.split('hysteria2://')[1];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let [__, password, server, ___, port, addons, name] =
|
||||
/^(.*?)@(.*?)(:(\d+))?\/?\?(.*?)(?:#(.*?))$/.exec(line);
|
||||
port = parseInt(`${port}`, 10);
|
||||
if (isNaN(port)) {
|
||||
port = 443;
|
||||
}
|
||||
password = decodeURIComponent(password);
|
||||
name = decodeURIComponent(name) ?? `Hysteria2 ${server}:${port}`;
|
||||
|
||||
const proxy = {
|
||||
type: 'hysteria2',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
password,
|
||||
};
|
||||
|
||||
const params = {};
|
||||
for (const addon of addons.split('&')) {
|
||||
const [key, valueRaw] = addon.split('=');
|
||||
let value = valueRaw;
|
||||
value = decodeURIComponent(valueRaw);
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
proxy.sni = params.sni;
|
||||
if (!proxy.sni && params.peer) {
|
||||
proxy.sni = params.peer;
|
||||
}
|
||||
if (params.obfs && params.obfs !== 'none') {
|
||||
proxy.obfs = params.obfs;
|
||||
}
|
||||
|
||||
proxy['obfs-password'] = params['obfs-password'];
|
||||
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
|
||||
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
|
||||
proxy['tls-fingerprint'] = params.pinSHA256;
|
||||
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
// Trojan URI format
|
||||
function URI_Trojan() {
|
||||
const name = 'URI Trojan Parser';
|
||||
@@ -237,36 +465,17 @@ 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;
|
||||
let [newLine, name] = line.split(/#(.+)/, 2);
|
||||
const parser = getTrojanURIParser();
|
||||
const proxy = parser.parse(newLine);
|
||||
if (isNotBlank(name)) {
|
||||
try {
|
||||
proxy.name = decodeURIComponent(name);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
@@ -281,7 +490,51 @@ function Clash_All() {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const parse = (line) => JSON.parse(line);
|
||||
const parse = (line) => {
|
||||
const proxy = JSON.parse(line);
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'wireguard',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
throw new Error(
|
||||
`Clash does not support proxy with type: ${proxy.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
// handle vmess sni
|
||||
if (['vmess', 'vless'].includes(proxy.type)) {
|
||||
proxy.sni = proxy.servername;
|
||||
delete proxy.servername;
|
||||
if (proxy.tls && !proxy.sni) {
|
||||
if (proxy.network === 'ws') {
|
||||
proxy.sni = proxy['ws-opts']?.headers?.Host;
|
||||
} else if (proxy.network === 'http') {
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
proxy.sni = Array.isArray(httpHost)
|
||||
? httpHost[0]
|
||||
: httpHost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (proxy['benchmark-url']) {
|
||||
proxy['test-url'] = proxy['benchmark-url'];
|
||||
}
|
||||
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
@@ -403,6 +656,15 @@ function Loon_Trojan() {
|
||||
const parse = (line) => getLoonParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
function Loon_Hysteria2() {
|
||||
const name = 'Loon Hysteria2 Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*Hysteria2/i.test(line.split(',')[0]);
|
||||
};
|
||||
|
||||
const parse = (line) => getLoonParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Loon_Http() {
|
||||
const name = 'Loon HTTP Parser';
|
||||
@@ -414,6 +676,113 @@ function Loon_Http() {
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Loon_WireGuard() {
|
||||
const name = 'Loon WireGuard Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*wireguard/i.test(line.split(',')[0]);
|
||||
};
|
||||
|
||||
const parse = (line) => {
|
||||
const name = line.match(
|
||||
/(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
|
||||
)?.[1];
|
||||
line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
|
||||
let peers = line.match(
|
||||
/,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
|
||||
)?.[1];
|
||||
let serverPort = peers.match(
|
||||
/(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
|
||||
);
|
||||
let server = serverPort?.[2];
|
||||
let port = parseInt(serverPort?.[3], 10);
|
||||
let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
|
||||
if (mtu) {
|
||||
mtu = parseInt(mtu, 10);
|
||||
}
|
||||
let keepalive = line.match(
|
||||
/(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
if (keepalive) {
|
||||
keepalive = parseInt(keepalive, 10);
|
||||
}
|
||||
let reserved = peers.match(
|
||||
/(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
if (reserved) {
|
||||
reserved = JSON.parse(reserved);
|
||||
}
|
||||
|
||||
let dns;
|
||||
let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
|
||||
let dnsv6 = line.match(
|
||||
/(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
if (dnsv4 || dnsv6) {
|
||||
dns = [];
|
||||
if (dnsv4) {
|
||||
dns.push(dnsv4);
|
||||
}
|
||||
if (dnsv6) {
|
||||
dns.push(dnsv6);
|
||||
}
|
||||
}
|
||||
let allowedIps = peers
|
||||
.match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
|
||||
?.split(',')
|
||||
.map((i) => i.trim());
|
||||
let preSharedKey = peers.match(
|
||||
/(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
let ip = line.match(
|
||||
/(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
let ipv6 = line.match(
|
||||
/(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
let publicKey = peers.match(
|
||||
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2];
|
||||
const proxy = {
|
||||
type: 'wireguard',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
ip,
|
||||
ipv6,
|
||||
'private-key': line.match(
|
||||
/(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
|
||||
)?.[2],
|
||||
'public-key': publicKey,
|
||||
mtu,
|
||||
keepalive,
|
||||
reserved,
|
||||
'allowed-ips': allowedIps,
|
||||
'preshared-key': preSharedKey,
|
||||
dns,
|
||||
udp: true,
|
||||
peers: [
|
||||
{
|
||||
server,
|
||||
port,
|
||||
ip,
|
||||
ipv6,
|
||||
'public-key': publicKey,
|
||||
'pre-shared-key': preSharedKey,
|
||||
allowed_ips: allowedIps,
|
||||
reserved,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
proxy;
|
||||
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
|
||||
proxy['remote-dns-resolve'] = true;
|
||||
}
|
||||
return proxy;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_SS() {
|
||||
const name = 'Surge SS Parser';
|
||||
const test = (line) => {
|
||||
@@ -465,7 +834,33 @@ function Surge_Socks5() {
|
||||
function Surge_Snell() {
|
||||
const name = 'Surge Snell Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*snell?/.test(line.split(',')[0]);
|
||||
return /^.*=\s*snell/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_Tuic() {
|
||||
const name = 'Surge Tuic Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
function Surge_WireGuard() {
|
||||
const name = 'Surge WireGuard Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*wireguard/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Surge_Hysteria2() {
|
||||
const name = 'Surge Hysteria2 Parser';
|
||||
const test = (line) => {
|
||||
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
|
||||
};
|
||||
const parse = (line) => getSurgeParser().parse(line);
|
||||
return { name, test, parse };
|
||||
@@ -475,6 +870,8 @@ export default [
|
||||
URI_SS(),
|
||||
URI_SSR(),
|
||||
URI_VMess(),
|
||||
URI_VLESS(),
|
||||
URI_Hysteria2(),
|
||||
URI_Trojan(),
|
||||
Clash_All(),
|
||||
Surge_SS(),
|
||||
@@ -482,13 +879,18 @@ export default [
|
||||
Surge_Trojan(),
|
||||
Surge_Http(),
|
||||
Surge_Snell(),
|
||||
Surge_Tuic(),
|
||||
Surge_WireGuard(),
|
||||
Surge_Hysteria2(),
|
||||
Surge_Socks5(),
|
||||
Loon_SS(),
|
||||
Loon_SSR(),
|
||||
Loon_VMess(),
|
||||
Loon_Vless(),
|
||||
Loon_Hysteria2(),
|
||||
Loon_Trojan(),
|
||||
Loon_Http(),
|
||||
Loon_WireGuard(),
|
||||
QX_SS(),
|
||||
QX_SSR(),
|
||||
QX_VMess(),
|
||||
|
||||
@@ -35,7 +35,7 @@ const grammars = String.raw`
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) {
|
||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
|
||||
// handle ssr obfs
|
||||
proxy.obfs = obfs.type;
|
||||
}
|
||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle ss obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -56,7 +56,7 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_ho
|
||||
}
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "auto";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
handleTransport();
|
||||
}
|
||||
@@ -68,6 +68,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
@@ -105,17 +108,16 @@ 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);
|
||||
}
|
||||
|
||||
method = comma cipher:cipher {
|
||||
if (cipher !== 'none') proxy.cipher = cipher;
|
||||
else proxy.cipher = '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"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
@@ -143,6 +145,9 @@ username = & {
|
||||
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
|
||||
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
|
||||
|
||||
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
|
||||
|
||||
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
|
||||
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
|
||||
@@ -168,6 +173,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) {
|
||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
|
||||
// handle ssr obfs
|
||||
proxy.obfs = obfs.type;
|
||||
}
|
||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle ss obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -54,7 +54,7 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_ho
|
||||
}
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "auto";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
handleTransport();
|
||||
}
|
||||
@@ -66,6 +66,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
@@ -103,17 +106,16 @@ 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);
|
||||
}
|
||||
|
||||
method = comma cipher:cipher {
|
||||
if (cipher !== 'none') proxy.cipher = cipher;
|
||||
else proxy.cipher = '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"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
||||
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"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
@@ -141,6 +143,9 @@ username = & {
|
||||
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
|
||||
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
|
||||
|
||||
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
|
||||
|
||||
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
|
||||
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
|
||||
@@ -166,6 +171,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
|
||||
@@ -43,13 +43,13 @@ start = (trojan/shadowsocks/vmess/http/socks5) {
|
||||
}
|
||||
|
||||
trojan = "trojan" equals address
|
||||
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* {
|
||||
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
shadowsocks = "shadowsocks" equals address
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* {
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
|
||||
if (proxy.protocol) {
|
||||
proxy.type = "ssr";
|
||||
// handle ssr obfs
|
||||
@@ -80,9 +80,9 @@ shadowsocks = "shadowsocks" equals address
|
||||
}
|
||||
|
||||
vmess = "vmess" equals address
|
||||
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
|
||||
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "auto";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
@@ -92,12 +92,12 @@ vmess = "vmess" equals address
|
||||
}
|
||||
|
||||
http = "http" equals address
|
||||
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{
|
||||
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
socks5 = "socks5" equals address
|
||||
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* {
|
||||
(username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -140,10 +140,9 @@ password = comma "password" equals password:[^=,]+ { proxy.password = password.j
|
||||
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); }
|
||||
|
||||
method = comma "method" equals cipher:cipher {
|
||||
if (cipher !== 'none') proxy.cipher = cipher;
|
||||
else proxy.cipher = 'auto';
|
||||
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; }
|
||||
@@ -156,6 +155,14 @@ tls_verification = comma "tls-verification" equals flag:bool {
|
||||
proxy["skip-cert-verify"] = !flag;
|
||||
}
|
||||
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
|
||||
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
|
||||
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
|
||||
proxy["tls-no-session-ticket"] = flag;
|
||||
}
|
||||
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
||||
proxy["tls-no-session-reuse"] = flag;
|
||||
}
|
||||
|
||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; }
|
||||
@@ -167,6 +174,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
|
||||
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
|
||||
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
|
||||
|
||||
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
|
||||
|
||||
uri = $[^,]+
|
||||
|
||||
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }
|
||||
|
||||
@@ -41,13 +41,13 @@ start = (trojan/shadowsocks/vmess/http/socks5) {
|
||||
}
|
||||
|
||||
trojan = "trojan" equals address
|
||||
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* {
|
||||
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
shadowsocks = "shadowsocks" equals address
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* {
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
|
||||
if (proxy.protocol) {
|
||||
proxy.type = "ssr";
|
||||
// handle ssr obfs
|
||||
@@ -78,9 +78,9 @@ shadowsocks = "shadowsocks" equals address
|
||||
}
|
||||
|
||||
vmess = "vmess" equals address
|
||||
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
|
||||
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "auto";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
@@ -90,12 +90,12 @@ vmess = "vmess" equals address
|
||||
}
|
||||
|
||||
http = "http" equals address
|
||||
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{
|
||||
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
socks5 = "socks5" equals address
|
||||
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* {
|
||||
(username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -138,10 +138,9 @@ password = comma "password" equals password:[^=,]+ { proxy.password = password.j
|
||||
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); }
|
||||
|
||||
method = comma "method" equals cipher:cipher {
|
||||
if (cipher !== 'none') proxy.cipher = cipher;
|
||||
else proxy.cipher = 'auto';
|
||||
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; }
|
||||
@@ -154,6 +153,14 @@ tls_verification = comma "tls-verification" equals flag:bool {
|
||||
proxy["skip-cert-verify"] = !flag;
|
||||
}
|
||||
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
|
||||
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
|
||||
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
|
||||
proxy["tls-no-session-ticket"] = flag;
|
||||
}
|
||||
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
||||
proxy["tls-no-session-reuse"] = flag;
|
||||
}
|
||||
|
||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; }
|
||||
@@ -165,6 +172,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
|
||||
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
|
||||
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
|
||||
|
||||
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
|
||||
|
||||
uri = $[^,]+
|
||||
|
||||
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }
|
||||
|
||||
@@ -25,15 +25,18 @@ const grammars = String.raw`
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
|
||||
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -43,9 +46,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "auto";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
@@ -53,18 +56,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
||||
}
|
||||
handleWebsocket();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http" address (username password)? (fast_open/others)* {
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "snell";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -73,10 +76,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
}
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "wireguard-surge";
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
}
|
||||
@@ -109,7 +125,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;
|
||||
}
|
||||
}
|
||||
@@ -147,15 +163,15 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
|
||||
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||
|
||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
method = comma "encrypt-method" equals cipher:cipher {
|
||||
if (cipher !== 'none') proxy.cipher = cipher;
|
||||
else proxy.cipher = 'auto';
|
||||
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:$[^,]+ {
|
||||
@@ -176,6 +192,22 @@ uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
|
||||
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
|
||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
||||
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
|
||||
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
|
||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
|
||||
@@ -23,15 +23,18 @@
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
|
||||
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
|
||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -41,9 +44,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "auto";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
@@ -51,18 +54,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
||||
}
|
||||
handleWebsocket();
|
||||
}
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
|
||||
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleWebsocket();
|
||||
}
|
||||
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http" address (username password)? (fast_open/others)* {
|
||||
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
|
||||
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "snell";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
@@ -71,10 +74,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
|
||||
tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
}
|
||||
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "tuic";
|
||||
proxy.version = 5;
|
||||
}
|
||||
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "wireguard-surge";
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
|
||||
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
proxy.tls = true;
|
||||
}
|
||||
@@ -107,7 +123,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;
|
||||
}
|
||||
}
|
||||
@@ -145,15 +161,15 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
|
||||
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||
|
||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
method = comma "encrypt-method" equals cipher:cipher {
|
||||
if (cipher !== 'none') proxy.cipher = cipher;
|
||||
else proxy.cipher = 'auto';
|
||||
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:$[^,]+ {
|
||||
@@ -174,6 +190,22 @@ uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
|
||||
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
|
||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
||||
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
|
||||
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
|
||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
|
||||
125
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js
Normal file
125
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js
Normal file
@@ -0,0 +1,125 @@
|
||||
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"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
115
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.peg
Normal file
115
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.peg
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
proxy.network = params["type"]
|
||||
if (params["path"]) {
|
||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
||||
}
|
||||
if (params["host"]) {
|
||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -13,17 +13,22 @@ function Base64Encoded() {
|
||||
const name = 'Base64 Pre-processor';
|
||||
|
||||
const keys = [
|
||||
'dm1lc3M',
|
||||
'c3NyOi8v',
|
||||
'dHJvamFu',
|
||||
'c3M6Ly',
|
||||
'c3NkOi8v',
|
||||
'c2hhZG93',
|
||||
'aHR0c',
|
||||
'dm1lc3M', // vmess
|
||||
'c3NyOi8v', // ssr://
|
||||
'dHJvamFu', // trojan
|
||||
'c3M6Ly', // ss:/
|
||||
'c3NkOi8v', // ssd://
|
||||
'c2hhZG93', // shadow
|
||||
'aHR0c', // htt
|
||||
'dmxlc3M=', // vless
|
||||
'aHlzdGVyaWEy', // hysteria2
|
||||
];
|
||||
|
||||
const test = function (raw) {
|
||||
return keys.some((k) => raw.indexOf(k) !== -1);
|
||||
return (
|
||||
!/^\w+:\/\/\w+/im.test(raw) &&
|
||||
keys.some((k) => raw.indexOf(k) !== -1)
|
||||
);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
raw = Base64.decode(raw);
|
||||
@@ -35,7 +40,9 @@ function Base64Encoded() {
|
||||
function Clash() {
|
||||
const name = 'Clash Pre-processor';
|
||||
const test = function (raw) {
|
||||
return /proxies/.test(raw);
|
||||
if (!/proxies/.test(raw)) return false;
|
||||
const content = safeLoad(raw);
|
||||
return content.proxies && Array.isArray(content.proxies);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
// Clash YAML format
|
||||
@@ -95,24 +102,12 @@ function FullConfig() {
|
||||
return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
const regex = /^\[server_local]|\[Proxy]/gm;
|
||||
const match = regex.exec(raw);
|
||||
const results = [];
|
||||
|
||||
let first = true;
|
||||
if (match) {
|
||||
raw = raw.substring(match.index);
|
||||
for (const line of raw.split('\n')) {
|
||||
if (!first && !line.test(/^\s*\[/)) {
|
||||
results.push(line);
|
||||
}
|
||||
// skip the first line
|
||||
first = false;
|
||||
}
|
||||
return results.join('\n');
|
||||
}
|
||||
const match = raw.match(
|
||||
/^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
|
||||
)?.[1];
|
||||
return match || raw;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [HTML(), Base64Encoded(), Clash(), SSD(), FullConfig()];
|
||||
export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import scriptResourceCache from '@/utils/script-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';
|
||||
import env from '@/utils/env';
|
||||
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
|
||||
|
||||
/**
|
||||
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 +27,7 @@ The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed a
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
function ConditionalFilter({ rule }) {
|
||||
return {
|
||||
@@ -290,7 +296,7 @@ function RegexDeleteOperator(regex) {
|
||||
1. This function name should be `operator`!
|
||||
2. Always declare variables before using them!
|
||||
*/
|
||||
function ScriptOperator(script, targetPlatform, $arguments) {
|
||||
function ScriptOperator(script, targetPlatform, $arguments, source) {
|
||||
return {
|
||||
name: 'Script Operator',
|
||||
func: async (proxies) => {
|
||||
@@ -301,7 +307,24 @@ function ScriptOperator(script, targetPlatform, $arguments) {
|
||||
script,
|
||||
$arguments,
|
||||
);
|
||||
output = operator(proxies, targetPlatform);
|
||||
output = operator(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
return output;
|
||||
},
|
||||
nodeFunc: async (proxies) => {
|
||||
let output = proxies;
|
||||
await (async function () {
|
||||
const operator = createDynamicFunction(
|
||||
'operator',
|
||||
`async function operator(proxies = []) {
|
||||
return proxies.map(($server = {}) => {
|
||||
${script}
|
||||
return $server
|
||||
})
|
||||
}`,
|
||||
$arguments,
|
||||
);
|
||||
output = operator(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
return output;
|
||||
},
|
||||
@@ -310,6 +333,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 +352,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 +369,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 +393,49 @@ 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;
|
||||
},
|
||||
Ali: async function (domain) {
|
||||
const id = hex_md5(`ALI:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
|
||||
domain,
|
||||
)}&type=A&short=1`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
});
|
||||
const answers = JSON.parse(resp.body);
|
||||
if (answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1];
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
Tencent: async function (domain) {
|
||||
const id = hex_md5(`ALI:${domain}`);
|
||||
const cached = resourceCache.get(id);
|
||||
if (cached) return cached;
|
||||
const resp = await $.http.get({
|
||||
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
|
||||
domain,
|
||||
)}`,
|
||||
headers: {
|
||||
accept: 'application/dns-json',
|
||||
},
|
||||
});
|
||||
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
|
||||
if (answers.length === 0) {
|
||||
throw new Error('No answers');
|
||||
}
|
||||
const result = answers[answers.length - 1];
|
||||
resourceCache.set(id, result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -373,7 +451,9 @@ function ResolveDomainOperator({ provider }) {
|
||||
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
|
||||
const totalDomain = [
|
||||
...new Set(
|
||||
proxies.filter((p) => !isIP(p.server)).map((c) => c.server),
|
||||
proxies
|
||||
.filter((p) => !isIP(p.server) && !p['no-resolve'])
|
||||
.map((c) => c.server),
|
||||
),
|
||||
];
|
||||
const totalBatch = Math.ceil(totalDomain.length / limit);
|
||||
@@ -397,8 +477,15 @@ function ResolveDomainOperator({ provider }) {
|
||||
}
|
||||
await Promise.all(currentBatch);
|
||||
}
|
||||
proxies.forEach((proxy) => {
|
||||
proxy.server = results[proxy.server] || proxy.server;
|
||||
proxies.forEach((p) => {
|
||||
if (!p['no-resolve']) {
|
||||
if (results[p.server]) {
|
||||
p.server = results[p.server];
|
||||
p.resolved = true;
|
||||
} else {
|
||||
p.resolved = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return proxies;
|
||||
@@ -503,7 +590,7 @@ function TypeFilter(types) {
|
||||
1. This function name should be `filter`!
|
||||
2. Always declare variables before using them!
|
||||
*/
|
||||
function ScriptFilter(script, targetPlatform, $arguments) {
|
||||
function ScriptFilter(script, targetPlatform, $arguments, source) {
|
||||
return {
|
||||
name: 'Script Filter',
|
||||
func: async (proxies) => {
|
||||
@@ -514,7 +601,7 @@ function ScriptFilter(script, targetPlatform, $arguments) {
|
||||
script,
|
||||
$arguments,
|
||||
);
|
||||
output = filter(proxies, targetPlatform);
|
||||
output = filter(proxies, targetPlatform, { source, ...env });
|
||||
})();
|
||||
return output;
|
||||
},
|
||||
@@ -547,7 +634,8 @@ async function ApplyFilter(filter, objs) {
|
||||
selected = await filter.func(objs);
|
||||
} catch (err) {
|
||||
// print log and skip this filter
|
||||
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
||||
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
||||
throw new Error(`脚本过滤失败 ${err.message ?? err}`);
|
||||
}
|
||||
return objs.filter((_, i) => selected[i]);
|
||||
}
|
||||
@@ -558,8 +646,33 @@ async function ApplyOperator(operator, objs) {
|
||||
const output_ = await operator.func(output);
|
||||
if (output_) output = output_;
|
||||
} catch (err) {
|
||||
// print log and skip this operator
|
||||
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
|
||||
$.error(
|
||||
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
||||
);
|
||||
let funcErr = '';
|
||||
let funcErrMsg = `${err.message ?? err}`;
|
||||
if (funcErrMsg.includes('$server is not defined')) {
|
||||
funcErr = '';
|
||||
} else {
|
||||
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
|
||||
}
|
||||
try {
|
||||
const output_ = await operator.nodeFunc(output);
|
||||
if (output_) output = output_;
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Cannot apply operator ${operator.name}(node script)! Reason: ${err}`,
|
||||
);
|
||||
let nodeErr = '';
|
||||
let nodeErrMsg = `${err.message ?? err}`;
|
||||
if (funcErr && nodeErrMsg === funcErrMsg) {
|
||||
nodeErr = '';
|
||||
funcErr = `执行失败 ${funcErrMsg}`;
|
||||
} else {
|
||||
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
|
||||
}
|
||||
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
@@ -606,6 +719,7 @@ function removeFlag(str) {
|
||||
}
|
||||
|
||||
function createDynamicFunction(name, script, $arguments) {
|
||||
const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer };
|
||||
if ($.env.isLoon) {
|
||||
return new Function(
|
||||
'$arguments',
|
||||
@@ -614,6 +728,9 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
'$persistentStore',
|
||||
'$httpClient',
|
||||
'$notification',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
'flowUtils',
|
||||
`${script}\n return ${name}`,
|
||||
)(
|
||||
$arguments,
|
||||
@@ -625,13 +742,20 @@ function createDynamicFunction(name, script, $arguments) {
|
||||
$httpClient,
|
||||
// eslint-disable-next-line no-undef
|
||||
$notification,
|
||||
ProxyUtils,
|
||||
scriptResourceCache,
|
||||
flowUtils,
|
||||
);
|
||||
} else {
|
||||
return new Function(
|
||||
'$arguments',
|
||||
'$substore',
|
||||
'lodash',
|
||||
'ProxyUtils',
|
||||
'scriptResourceCache',
|
||||
'flowUtils',
|
||||
|
||||
`${script}\n return ${name}`,
|
||||
)($arguments, $, lodash);
|
||||
)($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,115 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function Clash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
proxies.filter((proxy) => {
|
||||
if (proxy.type === 'vless') return false;
|
||||
// VLESS XTLS is not supported by Clash
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
||||
// github.com/Dreamacro/clash/pull/2891/files
|
||||
// filter unsupported proxies
|
||||
proxies = proxies.filter((proxy) => {
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'vless',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'wireguard',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||
(proxy.type === 'vless' &&
|
||||
(typeof proxy.flow !== 'undefined' ||
|
||||
proxy['reality-opts']))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === 'vless') {
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'http'
|
||||
) {
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.path') &&
|
||||
!Array.isArray(httpPath)
|
||||
) {
|
||||
proxy['http-opts'].path = [httpPath];
|
||||
}
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||
!Array.isArray(httpHost)
|
||||
) {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
if (proxy['tls-fingerprint']) {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
delete proxy['aead'];
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
|
||||
144
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
144
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function ClashMeta_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type) => {
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'tuic') {
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
} else {
|
||||
proxy.alpn = ['h3'];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
proxy.version = 5;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === 'vless') {
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'http'
|
||||
) {
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.path') &&
|
||||
!Array.isArray(httpPath)
|
||||
) {
|
||||
proxy['http-opts'].path = [httpPath];
|
||||
}
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||
!Array.isArray(httpHost)
|
||||
) {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
if (proxy['tls-fingerprint']) {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return proxy;
|
||||
});
|
||||
|
||||
return type === 'internal'
|
||||
? list
|
||||
: 'proxies:\n' +
|
||||
list
|
||||
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||
.join('');
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import Surge_Producer from './surge';
|
||||
import SurgeMac_Producer from './surgemac';
|
||||
import Clash_Producer from './clash';
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import Stash_Producer from './stash';
|
||||
import Loon_Producer from './loon';
|
||||
import URI_Producer from './uri';
|
||||
import V2Ray_Producer from './v2ray';
|
||||
import QX_Producer from './qx';
|
||||
import ShadowRocket_Producer from './shadowrocket';
|
||||
|
||||
function JSON_Producer() {
|
||||
const type = 'ALL';
|
||||
@@ -14,9 +18,13 @@ function JSON_Producer() {
|
||||
export default {
|
||||
QX: QX_Producer(),
|
||||
Surge: Surge_Producer(),
|
||||
SurgeMac: SurgeMac_Producer(),
|
||||
Loon: Loon_Producer(),
|
||||
Clash: Clash_Producer(),
|
||||
ClashMeta: ClashMeta_Producer(),
|
||||
URI: URI_Producer(),
|
||||
V2Ray: V2Ray_Producer(),
|
||||
JSON: JSON_Producer(),
|
||||
Stash: Stash_Producer(),
|
||||
ShadowRocket: ShadowRocket_Producer(),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
const targetPlatform = 'Loon';
|
||||
import { isPresent, Result } from './utils';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
|
||||
export default function Loon_Producer() {
|
||||
const produce = (proxy) => {
|
||||
@@ -17,6 +18,10 @@ export default function Loon_Producer() {
|
||||
return vless(proxy);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'wireguard':
|
||||
return wireguard(proxy);
|
||||
case 'hysteria2':
|
||||
return hysteria2(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
@@ -72,7 +77,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');
|
||||
@@ -98,7 +103,7 @@ function trojan(proxy) {
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -127,9 +132,7 @@ function trojan(proxy) {
|
||||
function vmess(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${
|
||||
proxy.cipher === 'auto' ? 'none' : proxy.cipher
|
||||
},"${proxy.uuid}"`,
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
||||
);
|
||||
|
||||
// transport
|
||||
@@ -137,21 +140,23 @@ function vmess(proxy) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts'].path}`,
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else if (proxy.network === 'http') {
|
||||
result.append(`,transport=http`);
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['http-opts'].path}`,
|
||||
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
|
||||
'http-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['http-opts'].headers.Host}`,
|
||||
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
|
||||
'http-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -189,6 +194,9 @@ function vmess(proxy) {
|
||||
}
|
||||
|
||||
function vless(proxy) {
|
||||
if (proxy['reality-opts']) {
|
||||
throw new Error(`reality is unsupported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
||||
@@ -199,21 +207,23 @@ function vless(proxy) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts'].path}`,
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else if (proxy.network === 'http') {
|
||||
result.append(`,transport=http`);
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['http-opts'].path}`,
|
||||
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
|
||||
'http-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['http-opts'].headers.Host}`,
|
||||
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
|
||||
'http-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -266,3 +276,93 @@ function http(proxy) {
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function wireguard(proxy) {
|
||||
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
|
||||
proxy.server = proxy.peers[0].server;
|
||||
proxy.port = proxy.peers[0].port;
|
||||
proxy.ip = proxy.peers[0].ip;
|
||||
proxy.ipv6 = proxy.peers[0].ipv6;
|
||||
proxy['public-key'] = proxy.peers[0]['public-key'];
|
||||
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
||||
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips'];
|
||||
proxy.reserved = proxy.peers[0].reserved;
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=wireguard`);
|
||||
|
||||
result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
|
||||
result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,private-key="${proxy['private-key']}"`,
|
||||
'private-key',
|
||||
);
|
||||
result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
|
||||
|
||||
if (proxy.dns) {
|
||||
if (Array.isArray(proxy.dns)) {
|
||||
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
|
||||
proxy.dns = proxy.dns.find((i) => isIPv4(i));
|
||||
}
|
||||
}
|
||||
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
|
||||
result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
|
||||
result.appendIfPresent(
|
||||
`,keepalive=${proxy['persistent-keepalive']}`,
|
||||
'persistent-keepalive',
|
||||
);
|
||||
result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
|
||||
const allowedIps = Array.isArray(proxy['allowed-ips'])
|
||||
? proxy['allowed-ips'].join(',')
|
||||
: proxy['allowed-ips'];
|
||||
let reserved = Array.isArray(proxy.reserved)
|
||||
? proxy.reserved.join(',')
|
||||
: proxy.reserved;
|
||||
if (reserved) {
|
||||
reserved = `,reserved=[${reserved}]`;
|
||||
}
|
||||
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
if (presharedKey) {
|
||||
presharedKey = `,preshared-key="${presharedKey}"`;
|
||||
}
|
||||
result.append(
|
||||
`,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
|
||||
allowedIps ?? '0.0.0.0/0,::/0'
|
||||
}",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
|
||||
presharedKey ?? ''
|
||||
}}]`,
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function hysteria2(proxy) {
|
||||
if (proxy.obfs || proxy['obfs-password']) {
|
||||
throw new Error(`obfs is unsupported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
||||
|
||||
// download-bandwidth
|
||||
result.appendIfPresent(
|
||||
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
|
||||
'down',
|
||||
);
|
||||
|
||||
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isPresent, Result } from './utils';
|
||||
|
||||
const targetPlatform = 'QX';
|
||||
|
||||
export default function QX_Producer() {
|
||||
@@ -61,18 +62,33 @@ function shadowsocks(proxy) {
|
||||
);
|
||||
}
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
|
||||
appendIfPresent(
|
||||
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
|
||||
'tls-no-session-ticket',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
|
||||
'tls-no-session-reuse',
|
||||
);
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -80,6 +96,12 @@ function shadowsocks(proxy) {
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
@@ -112,6 +134,12 @@ function shadowsocksr(proxy) {
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
@@ -132,11 +160,11 @@ function trojan(proxy) {
|
||||
if (needTls(proxy)) append(`,obfs=wss`);
|
||||
else append(`,obfs=ws`);
|
||||
appendIfPresent(
|
||||
`,obfs-uri=${proxy['ws-opts'].path}`,
|
||||
`,obfs-uri=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-host=${proxy['ws-opts'].headers.Host}`,
|
||||
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
@@ -149,18 +177,33 @@ function trojan(proxy) {
|
||||
append(`,over-tls=true`);
|
||||
}
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
|
||||
appendIfPresent(
|
||||
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
|
||||
'tls-no-session-ticket',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
|
||||
'tls-no-session-reuse',
|
||||
);
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -168,6 +211,12 @@ function trojan(proxy) {
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
@@ -180,7 +229,16 @@ function vmess(proxy) {
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`vmess=${proxy.server}:${proxy.port}`);
|
||||
append(`,method=${proxy.cipher === 'auto' ? 'none' : proxy.cipher}`);
|
||||
|
||||
// cipher
|
||||
let cipher;
|
||||
if (proxy.cipher === 'auto') {
|
||||
cipher = 'chacha20-ietf-poly1305';
|
||||
} else {
|
||||
cipher = proxy.cipher;
|
||||
}
|
||||
append(`,method=${cipher}`);
|
||||
|
||||
append(`,password=${proxy.uuid}`);
|
||||
|
||||
// obfs
|
||||
@@ -196,9 +254,18 @@ function vmess(proxy) {
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
appendIfPresent(`,obfs-uri=${proxy['ws-opts'].path}`, 'ws-opts.path');
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
appendIfPresent(
|
||||
`,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`,
|
||||
`,obfs-uri=${
|
||||
Array.isArray(transportPath) ? transportPath[0] : transportPath
|
||||
}`,
|
||||
`${proxy.network}-opts.path`,
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-host=${
|
||||
Array.isArray(transportHost) ? transportHost[0] : transportHost
|
||||
}`,
|
||||
`${proxy.network}-opts.headers.Host`,
|
||||
);
|
||||
} else {
|
||||
@@ -206,18 +273,33 @@ function vmess(proxy) {
|
||||
if (proxy.tls) append(`,obfs=over-tls`);
|
||||
}
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
|
||||
appendIfPresent(
|
||||
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
|
||||
'tls-no-session-ticket',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
|
||||
'tls-no-session-reuse',
|
||||
);
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
@@ -232,6 +314,12 @@ function vmess(proxy) {
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
@@ -253,18 +341,33 @@ function http(proxy) {
|
||||
}
|
||||
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
|
||||
appendIfPresent(
|
||||
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
|
||||
'tls-no-session-ticket',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
|
||||
'tls-no-session-reuse',
|
||||
);
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -272,6 +375,12 @@ function http(proxy) {
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
@@ -293,18 +402,33 @@ function socks5(proxy) {
|
||||
}
|
||||
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
if (needTls(proxy)) {
|
||||
appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
|
||||
appendIfPresent(
|
||||
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
|
||||
'tls-no-session-ticket',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
|
||||
'tls-no-session-reuse',
|
||||
);
|
||||
// tls fingerprint
|
||||
appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
// tls verification
|
||||
appendIfPresent(
|
||||
`,tls-verification=${!proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
|
||||
}
|
||||
|
||||
// tfo
|
||||
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
@@ -312,6 +436,12 @@ function socks5(proxy) {
|
||||
// udp
|
||||
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
@@ -319,11 +449,5 @@ function socks5(proxy) {
|
||||
}
|
||||
|
||||
function needTls(proxy) {
|
||||
return (
|
||||
proxy.tls ||
|
||||
proxy.sni ||
|
||||
typeof proxy['skip-cert-verify'] !== 'undefined' ||
|
||||
typeof proxy['tls-fingerprint'] !== 'undefined' ||
|
||||
typeof proxy['tls-host'] !== 'undefined'
|
||||
);
|
||||
return proxy.tls;
|
||||
}
|
||||
|
||||
163
backend/src/core/proxy-utils/producers/shadowrocket.js
Normal file
163
backend/src/core/proxy-utils/producers/shadowrocket.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function ShadowRocket_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.filter((proxy) => {
|
||||
if (
|
||||
proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'tuic') {
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
} else {
|
||||
proxy.alpn = ['h3'];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
proxy.version = 5;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria2') {
|
||||
if (
|
||||
proxy['obfs-password'] &&
|
||||
proxy.obfs == 'salamander'
|
||||
) {
|
||||
proxy.obfs = proxy['obfs-password'];
|
||||
delete proxy['obfs-password'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === 'vless') {
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'http'
|
||||
) {
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.path') &&
|
||||
!Array.isArray(httpPath)
|
||||
) {
|
||||
proxy['http-opts'].path = [httpPath];
|
||||
}
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||
!Array.isArray(httpHost)
|
||||
) {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
|
||||
if (proxy['tls-fingerprint']) {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -1,12 +1,220 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
export default function Stash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
return (
|
||||
'proxies:\n' +
|
||||
proxies
|
||||
.filter((proxy) => {
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'wireguard',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'snell' &&
|
||||
String(proxy.version) === '4') ||
|
||||
(proxy.type === 'vless' && proxy['reality-opts'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'tuic') {
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
} else {
|
||||
proxy.alpn = ['h3'];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
proxy.version = 5;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria2') {
|
||||
if (
|
||||
isPresent(proxy, 'password') &&
|
||||
!isPresent(proxy, 'auth')
|
||||
) {
|
||||
proxy.auth = proxy.password;
|
||||
delete proxy.password;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === 'vless') {
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['vmess', 'vless'].includes(proxy.type) &&
|
||||
proxy.network === 'http'
|
||||
) {
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.path') &&
|
||||
!Array.isArray(httpPath)
|
||||
) {
|
||||
proxy['http-opts'].path = [httpPath];
|
||||
}
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
if (
|
||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||
!Array.isArray(httpHost)
|
||||
) {
|
||||
proxy['http-opts'].headers.Host = [httpHost];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
if (proxy['tls-fingerprint']) {
|
||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||
}
|
||||
delete proxy['tls-fingerprint'];
|
||||
delete proxy['aead'];
|
||||
|
||||
if (proxy['test-url']) {
|
||||
proxy['benchmark-url'] = proxy['test-url'];
|
||||
delete proxy['test-url'];
|
||||
}
|
||||
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
if (
|
||||
['grpc'].includes(proxy.network) &&
|
||||
proxy[`${proxy.network}-opts`]
|
||||
) {
|
||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||
}
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('')
|
||||
|
||||
@@ -4,6 +4,14 @@ import $ from '@/core/app';
|
||||
|
||||
const targetPlatform = 'Surge';
|
||||
|
||||
const ipVersions = {
|
||||
dual: 'dual',
|
||||
ipv4: 'v4-only',
|
||||
ipv6: 'v6-only',
|
||||
'ipv4-prefer': 'prefer-v4',
|
||||
'ipv6-prefer': 'prefer-v6',
|
||||
};
|
||||
|
||||
export default function Surge_Producer() {
|
||||
const produce = (proxy) => {
|
||||
switch (proxy.type) {
|
||||
@@ -19,6 +27,12 @@ export default function Surge_Producer() {
|
||||
return socks5(proxy);
|
||||
case 'snell':
|
||||
return snell(proxy);
|
||||
case 'tuic':
|
||||
return tuic(proxy);
|
||||
case 'wireguard-surge':
|
||||
return wireguard(proxy);
|
||||
case 'hysteria2':
|
||||
return hysteria2(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
@@ -33,6 +47,16 @@ function shadowsocks(proxy) {
|
||||
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// obfs
|
||||
if (isPresent(proxy, 'plugin')) {
|
||||
if (proxy.plugin === 'obfs') {
|
||||
@@ -55,6 +79,33 @@ function shadowsocks(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -63,6 +114,16 @@ function trojan(proxy) {
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// transport
|
||||
handleTransport(result, proxy);
|
||||
|
||||
@@ -87,6 +148,33 @@ function trojan(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -95,6 +183,16 @@ function vmess(proxy) {
|
||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// transport
|
||||
handleTransport(result, proxy);
|
||||
|
||||
@@ -127,6 +225,32 @@ function vmess(proxy) {
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -137,6 +261,16 @@ function http(proxy) {
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// tls fingerprint
|
||||
result.appendIfPresent(
|
||||
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
|
||||
@@ -155,6 +289,33 @@ function http(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -165,6 +326,16 @@ function socks5(proxy) {
|
||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// tls fingerprint
|
||||
result.appendIfPresent(
|
||||
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
|
||||
@@ -185,6 +356,33 @@ function socks5(proxy) {
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -194,22 +392,263 @@ function snell(proxy) {
|
||||
result.appendIfPresent(`,version=${proxy.version}`, 'version');
|
||||
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// obfs
|
||||
result.appendIfPresent(
|
||||
`,obfs=${proxy['obfs-opts'].mode}`,
|
||||
`,obfs=${proxy['obfs-opts']?.mode}`,
|
||||
'obfs-opts.mode',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,obfs-host=${proxy['obfs-opts'].host}`,
|
||||
`,obfs-host=${proxy['obfs-opts']?.host}`,
|
||||
'obfs-opts.host',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,obfs-uri=${proxy['obfs-opts'].path}`,
|
||||
`,obfs-uri=${proxy['obfs-opts']?.path}`,
|
||||
'obfs-opts.path',
|
||||
);
|
||||
|
||||
// udp
|
||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
// reuse
|
||||
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function tuic(proxy) {
|
||||
const result = new Result(proxy);
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
let type = proxy.type;
|
||||
if (!proxy.token || proxy.token.length === 0) {
|
||||
type = 'tuic-v5';
|
||||
}
|
||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
result.appendIfPresent(`,token=${proxy.token}`, 'token');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
|
||||
'alpn',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// tls fingerprint
|
||||
result.appendIfPresent(
|
||||
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tfo
|
||||
if (isPresent(proxy, 'tfo')) {
|
||||
result.append(`,tfo=${proxy['tfo']}`);
|
||||
} else if (isPresent(proxy, 'fast-open')) {
|
||||
result.append(`,tfo=${proxy['fast-open']}`);
|
||||
}
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function wireguard(proxy) {
|
||||
const result = new Result(proxy);
|
||||
|
||||
result.append(`${proxy.name}=wireguard`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,section-name=${proxy['section-name']}`,
|
||||
'section-name',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function hysteria2(proxy) {
|
||||
if (proxy.obfs || proxy['obfs-password']) {
|
||||
throw new Error(`obfs is unsupported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
||||
'ip-version',
|
||||
);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||
'no-error-alert',
|
||||
);
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
|
||||
// tfo
|
||||
if (isPresent(proxy, 'tfo')) {
|
||||
result.append(`,tfo=${proxy['tfo']}`);
|
||||
} else if (isPresent(proxy, 'fast-open')) {
|
||||
result.append(`,tfo=${proxy['fast-open']}`);
|
||||
}
|
||||
|
||||
// test-url
|
||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||
|
||||
// shadow-tls
|
||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
|
||||
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
|
||||
'shadow-tls-version',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||
'shadow-tls-sni',
|
||||
);
|
||||
}
|
||||
|
||||
// block-quic
|
||||
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||
|
||||
// underlying-proxy
|
||||
result.appendIfPresent(
|
||||
`,underlying-proxy=${proxy['underlying-proxy']}`,
|
||||
'underlying-proxy',
|
||||
);
|
||||
|
||||
// download-bandwidth
|
||||
result.appendIfPresent(
|
||||
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
|
||||
'down',
|
||||
);
|
||||
|
||||
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -225,7 +664,13 @@ function handleTransport(result, proxy) {
|
||||
if (isPresent(proxy, 'ws-opts.headers')) {
|
||||
const headers = proxy['ws-opts'].headers;
|
||||
const value = Object.keys(headers)
|
||||
.map((k) => `${k}:${headers[k]}`)
|
||||
.map((k) => {
|
||||
let v = headers[k];
|
||||
if (['Host'].includes(k)) {
|
||||
v = `"${v}"`;
|
||||
}
|
||||
return `${k}:${v}`;
|
||||
})
|
||||
.join('|');
|
||||
if (isNotBlank(value)) {
|
||||
result.append(`,ws-headers=${value}`);
|
||||
|
||||
65
backend/src/core/proxy-utils/producers/surgemac.js
Normal file
65
backend/src/core/proxy-utils/producers/surgemac.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Result } from './utils';
|
||||
import Surge_Producer from './surge';
|
||||
|
||||
const targetPlatform = 'SurgeMac';
|
||||
|
||||
const surge_Producer = Surge_Producer();
|
||||
|
||||
export default function SurgeMac_Producer() {
|
||||
const produce = (proxy) => {
|
||||
switch (proxy.type) {
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
case 'ss':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'trojan':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'vmess':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'http':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'socks5':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'snell':
|
||||
return surge_Producer.produce(proxy);
|
||||
case 'tuic':
|
||||
return surge_Producer.produce(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocksr(proxy) {
|
||||
const result = new Result(proxy);
|
||||
|
||||
proxy.local_port = '__SubStoreLocalPort__';
|
||||
proxy.local_address = proxy.local_address ?? '127.0.0.1';
|
||||
|
||||
result.append(
|
||||
`${proxy.name} = external, exec = "${
|
||||
proxy.exec || '/usr/local/bin/ssr-local'
|
||||
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
|
||||
);
|
||||
|
||||
for (const [key, value] of Object.entries({
|
||||
cipher: '-m',
|
||||
obfs: '-o',
|
||||
password: '-k',
|
||||
port: '-p',
|
||||
protocol: '-O',
|
||||
'protocol-param': '-G',
|
||||
server: '-s',
|
||||
local_port: '-l',
|
||||
local_address: '-b',
|
||||
})) {
|
||||
result.appendIfPresent(
|
||||
`, args = "${value}", args = "${proxy[key]}"`,
|
||||
key,
|
||||
);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
import { isIPv6 } from '@/utils';
|
||||
|
||||
export default function URI_Producer() {
|
||||
const type = 'SINGLE';
|
||||
const produce = (proxy) => {
|
||||
let result = '';
|
||||
if (proxy.server && isIPv6(proxy.server)) {
|
||||
proxy.server = `[${proxy.server}]`;
|
||||
}
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
||||
@@ -55,27 +59,210 @@ export default function URI_Producer() {
|
||||
break;
|
||||
case 'vmess':
|
||||
// V2RayN URI format
|
||||
let type = '';
|
||||
let net = proxy.network || 'tcp';
|
||||
if (proxy.network === 'http') {
|
||||
net = 'tcp';
|
||||
type = 'http';
|
||||
}
|
||||
result = {
|
||||
v: '2',
|
||||
ps: proxy.name,
|
||||
add: proxy.server,
|
||||
port: proxy.port,
|
||||
id: proxy.uuid,
|
||||
type: '',
|
||||
type,
|
||||
aid: 0,
|
||||
net: proxy.network || 'tcp',
|
||||
net,
|
||||
tls: proxy.tls ? 'tls' : '',
|
||||
};
|
||||
if (proxy.tls && proxy.sni) {
|
||||
result.sni = proxy.sni;
|
||||
}
|
||||
// obfs
|
||||
if (proxy.network === 'ws') {
|
||||
result.path = proxy['ws-opts'].path || '/';
|
||||
result.host = proxy['ws-opts'].headers.Host || proxy.server;
|
||||
if (proxy.network) {
|
||||
let vmessTransportPath =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let vmessTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (vmessTransportPath) {
|
||||
result.path = Array.isArray(vmessTransportPath)
|
||||
? vmessTransportPath[0]
|
||||
: vmessTransportPath;
|
||||
}
|
||||
if (vmessTransportHost) {
|
||||
result.host = Array.isArray(vmessTransportHost)
|
||||
? vmessTransportHost[0]
|
||||
: vmessTransportHost;
|
||||
}
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
result.path =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
'grpc-service-name'
|
||||
];
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
result.type =
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||
'gun';
|
||||
}
|
||||
}
|
||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||
break;
|
||||
case 'vless':
|
||||
let security = 'none';
|
||||
const isReality = proxy['reality-opts'];
|
||||
let sid = '';
|
||||
let pbk = '';
|
||||
if (isReality) {
|
||||
security = 'reality';
|
||||
const publicKey = proxy['reality-opts']?.['public-key'];
|
||||
if (publicKey) {
|
||||
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
|
||||
}
|
||||
const shortId = proxy['reality-opts']?.['short-id'];
|
||||
if (shortId) {
|
||||
sid = `&sid=${encodeURIComponent(shortId)}`;
|
||||
}
|
||||
} else if (proxy.tls) {
|
||||
security = 'tls';
|
||||
}
|
||||
let alpn = '';
|
||||
if (proxy.alpn) {
|
||||
alpn = `&alpn=${encodeURIComponent(
|
||||
Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: proxy.alpn.join(','),
|
||||
)}`;
|
||||
}
|
||||
let allowInsecure = '';
|
||||
if (proxy['skip-cert-verify']) {
|
||||
allowInsecure = `&allowInsecure=1`;
|
||||
}
|
||||
let sni = '';
|
||||
if (proxy.sni) {
|
||||
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
|
||||
}
|
||||
let fp = '';
|
||||
if (proxy['client-fingerprint']) {
|
||||
fp = `&fp=${encodeURIComponent(
|
||||
proxy['client-fingerprint'],
|
||||
)}`;
|
||||
}
|
||||
let flow = '';
|
||||
if (proxy.flow) {
|
||||
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
|
||||
}
|
||||
let vlessTransport = `&type=${encodeURIComponent(
|
||||
proxy.network,
|
||||
)}`;
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
vlessTransport += `&mode=${encodeURIComponent(
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
|
||||
)}`;
|
||||
}
|
||||
|
||||
let vlessTransportServiceName =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
`${proxy.network}-service-name`
|
||||
];
|
||||
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let vlessTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (vlessTransportPath) {
|
||||
vlessTransport += `&path=${encodeURIComponent(
|
||||
Array.isArray(vlessTransportPath)
|
||||
? vlessTransportPath[0]
|
||||
: vlessTransportPath,
|
||||
)}`;
|
||||
}
|
||||
if (vlessTransportHost) {
|
||||
vlessTransport += `&host=${encodeURIComponent(
|
||||
Array.isArray(vlessTransportHost)
|
||||
? vlessTransportHost[0]
|
||||
: vlessTransportHost,
|
||||
)}`;
|
||||
}
|
||||
if (vlessTransportServiceName) {
|
||||
vlessTransport += `&serviceName=${encodeURIComponent(
|
||||
vlessTransportServiceName,
|
||||
)}`;
|
||||
}
|
||||
|
||||
result = `vless://${proxy.uuid}@${proxy.server}:${
|
||||
proxy.port
|
||||
}?${vlessTransport}&security=${encodeURIComponent(
|
||||
security,
|
||||
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
case 'trojan':
|
||||
let trojanTransport = '';
|
||||
if (proxy.network) {
|
||||
trojanTransport = `&type=${proxy.network}`;
|
||||
let trojanTransportPath =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let trojanTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (trojanTransportPath) {
|
||||
trojanTransport += `&path=${encodeURIComponent(
|
||||
Array.isArray(trojanTransportPath)
|
||||
? trojanTransportPath[0]
|
||||
: trojanTransportPath,
|
||||
)}`;
|
||||
}
|
||||
if (trojanTransportHost) {
|
||||
trojanTransport += `&host=${encodeURIComponent(
|
||||
Array.isArray(trojanTransportHost)
|
||||
? trojanTransportHost[0]
|
||||
: trojanTransportHost,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
result = `trojan://${proxy.password}@${proxy.server}:${
|
||||
proxy.port
|
||||
}#${encodeURIComponent(proxy.name)}`;
|
||||
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
|
||||
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
|
||||
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
|
||||
break;
|
||||
case 'hysteria2':
|
||||
let hysteria2params = [];
|
||||
if (proxy['skip-cert-verify']) {
|
||||
hysteria2params.push(`insecure=1`);
|
||||
}
|
||||
if (proxy.obfs) {
|
||||
hysteria2params.push(
|
||||
`obfs=${encodeURIComponent(proxy.obfs)}`,
|
||||
);
|
||||
if (proxy['obfs-password']) {
|
||||
hysteria2params.push(
|
||||
`obfs-password=${encodeURIComponent(
|
||||
proxy['obfs-password'],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (proxy.sni) {
|
||||
hysteria2params.push(
|
||||
`sni=${encodeURIComponent(proxy.sni)}`,
|
||||
);
|
||||
}
|
||||
if (proxy['tls-fingerprint']) {
|
||||
hysteria2params.push(
|
||||
`pinSHA256=${encodeURIComponent(
|
||||
proxy['tls-fingerprint'],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
if (proxy.tfo) {
|
||||
hysteria2params.push(`fastopen=1`);
|
||||
}
|
||||
result = `hysteria2://${proxy.password}@${proxy.server}:${
|
||||
proxy.port
|
||||
}?${hysteria2params.join('&')}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
|
||||
12
backend/src/core/proxy-utils/producers/v2ray.js
Normal file
12
backend/src/core/proxy-utils/producers/v2ray.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
import URI_Producer from './uri';
|
||||
|
||||
const URI = URI_Producer();
|
||||
|
||||
export default function V2Ray_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) =>
|
||||
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
|
||||
return { type, produce };
|
||||
}
|
||||
@@ -47,7 +47,7 @@ function AllRuleParser() {
|
||||
}
|
||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
||||
* @author: Peng-YM
|
||||
* @github: https://github.com/Peng-YM/Sub-Store
|
||||
* @github: https://github.com/sub-store-org/Sub-Store
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
import { version } from '../package.json';
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import { syncToGist, produceArtifact } from '@/restful/artifacts';
|
||||
import { version } from '../../package.json';
|
||||
import { ARTIFACTS_KEY } from '@/constants';
|
||||
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { syncToGist } from '@/restful/artifacts';
|
||||
|
||||
console.log(
|
||||
`
|
||||
!(async function () {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
// if GitHub token is not configured
|
||||
if (!settings.githubUser || !settings.gistToken) return;
|
||||
|
||||
const artifacts = $.read(ARTIFACTS_KEY);
|
||||
if (!artifacts || artifacts.length === 0) return;
|
||||
|
||||
const shouldSync = artifacts.some((artifact) => artifact.sync);
|
||||
if (shouldSync) await doSync();
|
||||
})().finally(() => $.done());
|
||||
|
||||
async function doSync() {
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
Sub-Store Sync -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
!(async function () {
|
||||
);
|
||||
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const files = {};
|
||||
@@ -37,18 +51,20 @@ console.log(
|
||||
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);
|
||||
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '全部订阅同步成功!');
|
||||
$.notify('🌍 Sub-Store', '全部订阅同步成功!');
|
||||
} catch (err) {
|
||||
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '同步订阅失败', `原因:${err}`);
|
||||
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`);
|
||||
$.error(`无法同步订阅配置到 Gist,原因:${err}`);
|
||||
}
|
||||
})().finally(() => $.done());
|
||||
}
|
||||
|
||||
43
backend/src/products/sub-store-0.js
Normal file
43
backend/src/products/sub-store-0.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 路由拆分 - 本文件只包含不涉及到解析器的 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';
|
||||
import registerFileRoutes from '@/restful/file';
|
||||
import registerModuleRoutes from '@/restful/module';
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
|
||||
function serve() {
|
||||
const $app = express({ substore: $ });
|
||||
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerFileRoutes($app);
|
||||
registerModuleRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerSortRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
|
||||
$app.start();
|
||||
}
|
||||
39
backend/src/products/sub-store-1.js
Normal file
39
backend/src/products/sub-store-1.js
Normal 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();
|
||||
}
|
||||
@@ -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,22 +12,22 @@ import {
|
||||
RequestInvalidError,
|
||||
ResourceNotFoundError,
|
||||
} from '@/restful/errors';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
export default function register($app) {
|
||||
// Initialization
|
||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||
|
||||
// RESTful APIs
|
||||
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
|
||||
$app.route('/api/artifacts')
|
||||
.get(getAllArtifacts)
|
||||
.post(createArtifact)
|
||||
.put(replaceArtifact);
|
||||
|
||||
$app.route('/api/artifact/:name')
|
||||
.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) {
|
||||
@@ -42,6 +35,12 @@ function getAllArtifacts(req, res) {
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
|
||||
function replaceArtifact(req, res) {
|
||||
const allArtifacts = req.body;
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
async function getArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
@@ -141,7 +140,12 @@ async function deleteArtifact(req, res) {
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: '',
|
||||
};
|
||||
await syncToGist(files);
|
||||
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
|
||||
try {
|
||||
await syncToGist(files);
|
||||
} catch (i) {
|
||||
$.error(`Function syncToGist: ${name} : ${i}`);
|
||||
}
|
||||
}
|
||||
// delete local cache
|
||||
deleteByName(allArtifacts, name);
|
||||
@@ -160,111 +164,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,157 +180,4 @@ async function syncToGist(files) {
|
||||
return manager.upload(files);
|
||||
}
|
||||
|
||||
async function produceArtifact({ type, name, platform }) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
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(
|
||||
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
|
||||
'⚠️ 订阅包含重复节点!',
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'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(
|
||||
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
|
||||
'⚠️ 订阅包含重复节点!',
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'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 };
|
||||
|
||||
@@ -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';
|
||||
@@ -14,13 +14,24 @@ export default function register($app) {
|
||||
|
||||
$app.route('/api/collections')
|
||||
.get(getAllCollections)
|
||||
.post(createCollection);
|
||||
.post(createCollection)
|
||||
.put(replaceCollection);
|
||||
}
|
||||
|
||||
// collection API
|
||||
function createCollection(req, res) {
|
||||
const collection = req.body;
|
||||
$.info(`正在创建组合订阅:${collection.name}`);
|
||||
if (/\//.test(collection.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_NAME',
|
||||
`Collection ${collection.name} is invalid`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
if (findByName(allCols, collection.name)) {
|
||||
failed(
|
||||
@@ -30,6 +41,7 @@ function createCollection(req, res) {
|
||||
`Collection ${collection.name} already exists.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
allCols.push(collection);
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
@@ -67,6 +79,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);
|
||||
@@ -96,3 +123,9 @@ function getAllCollections(req, res) {
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
success(res, allCols);
|
||||
}
|
||||
|
||||
function replaceCollection(req, res) {
|
||||
const allCols = req.body;
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -20,6 +20,23 @@ async function downloadSubscription(req, res) {
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
$.info(`正在下载订阅:${name}`);
|
||||
let { url, ua, content, mergeSources } = req.query;
|
||||
if (url) {
|
||||
url = decodeURIComponent(url);
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
}
|
||||
if (ua) {
|
||||
ua = decodeURIComponent(ua);
|
||||
$.info(`指定远程订阅 User-Agent: ${ua}`);
|
||||
}
|
||||
if (content) {
|
||||
content = decodeURIComponent(content);
|
||||
$.info(`指定本地订阅: ${content}`);
|
||||
}
|
||||
if (mergeSources) {
|
||||
mergeSources = decodeURIComponent(mergeSources);
|
||||
$.info(`指定合并来源: ${mergeSources}`);
|
||||
}
|
||||
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
@@ -29,13 +46,25 @@ async function downloadSubscription(req, res) {
|
||||
type: 'subscription',
|
||||
name,
|
||||
platform,
|
||||
url,
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
});
|
||||
|
||||
if (sub.source !== 'local') {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
if (sub.source !== 'local' || url) {
|
||||
try {
|
||||
// forward flow headers
|
||||
const flowInfo = await getFlowHeaders(url || sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
|
||||
err,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,22 +77,22 @@ async function downloadSubscription(req, res) {
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
|
||||
`🌍 Sub-Store 下载订阅失败`,
|
||||
`❌ 无法下载订阅:${name}!`,
|
||||
`🤔 原因:${JSON.stringify(err)}`,
|
||||
`🤔 原因:${err.message ?? err}`,
|
||||
);
|
||||
$.error(JSON.stringify(err));
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download subscription: ${name}`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
@@ -101,9 +130,17 @@ async function downloadCollection(req, res) {
|
||||
if (subnames.length > 0) {
|
||||
const sub = findByName(allSubs, subnames[0]);
|
||||
if (sub.source !== 'local') {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
try {
|
||||
const flowInfo = await getFlowHeaders(sub.url);
|
||||
if (flowInfo) {
|
||||
res.set('subscription-userinfo', flowInfo);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`组合订阅 ${name} 中的子订阅 ${
|
||||
sub.name
|
||||
} 获取流量信息时发生错误: ${err.message ?? err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +154,7 @@ async function downloadCollection(req, res) {
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||
`🌍 Sub-Store 下载组合订阅失败`,
|
||||
`❌ 下载组合订阅错误:${name}!`,
|
||||
`🤔 原因:${err}`,
|
||||
);
|
||||
@@ -126,13 +163,13 @@ async function downloadCollection(req, res) {
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download collection: ${name}`,
|
||||
`Reason: ${JSON.stringify(err)}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.notify(
|
||||
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||
`🌍 Sub-Store 下载组合订阅失败`,
|
||||
`❌ 未找到组合订阅:${name}!`,
|
||||
);
|
||||
failed(
|
||||
|
||||
109
backend/src/restful/file.js
Normal file
109
backend/src/restful/file.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { FILES_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||
|
||||
$app.route('/api/file/:name')
|
||||
.get(getFile)
|
||||
.patch(updateFile)
|
||||
.delete(deleteFile);
|
||||
|
||||
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||||
}
|
||||
|
||||
// file API
|
||||
function createFile(req, res) {
|
||||
const file = req.body;
|
||||
file.name = `${file.name ?? Date.now()}`;
|
||||
$.info(`正在创建文件:${file.name}`);
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
if (findByName(allFiles, file.name)) {
|
||||
return failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
req.body.name
|
||||
? `已存在 name 为 ${file.name} 的文件`
|
||||
: `无法同时创建相同的文件 可稍后重试`,
|
||||
),
|
||||
);
|
||||
}
|
||||
allFiles.push(file);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res, file, 201);
|
||||
}
|
||||
|
||||
function getFile(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (file) {
|
||||
res.status(200).json(file.content);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
`FILE_NOT_FOUND`,
|
||||
`File ${name} does not exist`,
|
||||
404,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFile(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
let file = req.body;
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const oldFile = findByName(allFiles, name);
|
||||
if (oldFile) {
|
||||
const newFile = {
|
||||
...oldFile,
|
||||
...file,
|
||||
};
|
||||
$.info(`正在更新文件:${name}...`);
|
||||
|
||||
updateByName(allFiles, name, newFile);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res, newFile);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`File ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteFile(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
$.info(`正在删除文件:${name}`);
|
||||
let allFiles = $.read(FILES_KEY);
|
||||
deleteByName(allFiles, name);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
function getAllFiles(req, res) {
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
success(
|
||||
res, // eslint-disable-next-line no-unused-vars
|
||||
allFiles.map(({ content, ...rest }) => rest),
|
||||
);
|
||||
}
|
||||
|
||||
function replaceFile(req, res) {
|
||||
const allFiles = req.body;
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res);
|
||||
}
|
||||
@@ -1,28 +1,27 @@
|
||||
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 registerFileRoutes from './file';
|
||||
import registerModuleRoutes from './module';
|
||||
import registerSyncRoutes from './sync';
|
||||
import registerDownloadRoutes from './download';
|
||||
import registerSettingRoutes from './settings';
|
||||
import registerPreviewRoutes from './preview';
|
||||
import registerSortingRoutes from './sort';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
|
||||
import registerMiscRoutes from './miscs';
|
||||
import registerNodeInfoRoutes from './node-info';
|
||||
|
||||
export default function serve() {
|
||||
const $app = express({ substore: $ });
|
||||
|
||||
let port;
|
||||
let host;
|
||||
if ($.env.isNode) {
|
||||
port = eval('process.env.SUB_STORE_BACKEND_API_PORT');
|
||||
host = eval('process.env.SUB_STORE_BACKEND_API_HOST');
|
||||
}
|
||||
const $app = express({ substore: $, port, host });
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
@@ -31,133 +30,11 @@ export default function serve() {
|
||||
registerSortingRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
|
||||
// utils
|
||||
$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/backup', gistBackup); // gist backup actions
|
||||
|
||||
// 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');
|
||||
});
|
||||
registerFileRoutes($app);
|
||||
registerModuleRoutes($app);
|
||||
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 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 IP_API(req, res) {
|
||||
const server = decodeURIComponent(req.params.server);
|
||||
const $http = HTTP();
|
||||
const result = await $http
|
||||
.get(`http://ip-api.com/json/${server}?lang=zh-CN`)
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
res.json(result);
|
||||
}
|
||||
|
||||
165
backend/src/restful/miscs.js
Normal file
165
backend/src/restful/miscs.js
Normal file
@@ -0,0 +1,165 @@
|
||||
import $ from '@/core/app';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
import { failed, success } from '@/restful/response';
|
||||
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';
|
||||
import env from '@/utils/env';
|
||||
|
||||
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.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
'attachment; filename="sub-store.json"',
|
||||
)
|
||||
.send(
|
||||
$.env.isNode
|
||||
? JSON.stringify($.cache)
|
||||
: $.read('#sub-store'),
|
||||
);
|
||||
})
|
||||
.post((req, res) => {
|
||||
const { content } = req.body;
|
||||
$.write(content, '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
$.cache = JSON.parse(content);
|
||||
$.persistCache();
|
||||
}
|
||||
migrate();
|
||||
success(res);
|
||||
});
|
||||
|
||||
// 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) {
|
||||
success(res, env);
|
||||
}
|
||||
|
||||
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);
|
||||
try {
|
||||
if (
|
||||
Object.keys(JSON.parse(content).settings).length ===
|
||||
0
|
||||
) {
|
||||
throw new Error(
|
||||
'备份文件应该至少包含 settings 字段',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Gist 备份文件校验失败, 无法还原\nReason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
throw new Error('Gist 备份文件校验失败, 无法还原');
|
||||
}
|
||||
// restore settings
|
||||
$.write(content, '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
content = JSON.parse(content);
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
$.info(`perform migration after restoring from gist...`);
|
||||
migrate();
|
||||
$.info(`migration completed`);
|
||||
$.info(`还原备份完成`);
|
||||
break;
|
||||
}
|
||||
success(res);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'BACKUP_FAILED',
|
||||
`Failed to ${action} data to gist!`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
backend/src/restful/module.js
Normal file
114
backend/src/restful/module.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { MODULES_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
|
||||
|
||||
$app.route('/api/module/:name')
|
||||
.get(getModule)
|
||||
.patch(updateModule)
|
||||
.delete(deleteModule);
|
||||
|
||||
$app.route('/api/modules')
|
||||
.get(getAllModules)
|
||||
.post(createModule)
|
||||
.put(replaceModule);
|
||||
}
|
||||
|
||||
// module API
|
||||
function createModule(req, res) {
|
||||
const module = req.body;
|
||||
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
|
||||
$.info(`正在创建模块:${module.name}`);
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
if (findByName(allModules, module.name)) {
|
||||
return failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
req.body.name
|
||||
? `已存在 name 为 ${module.name} 的模块`
|
||||
: `已存在相同的模块 请勿重复添加`,
|
||||
),
|
||||
);
|
||||
}
|
||||
allModules.push(module);
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res, module, 201);
|
||||
}
|
||||
|
||||
function getModule(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
const module = findByName(allModules, name);
|
||||
if (module) {
|
||||
res.status(200).json(module.content);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
`MODULE_NOT_FOUND`,
|
||||
`Module ${name} does not exist`,
|
||||
404,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateModule(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
let module = req.body;
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
const oldModule = findByName(allModules, name);
|
||||
if (oldModule) {
|
||||
const newModule = {
|
||||
...oldModule,
|
||||
...module,
|
||||
};
|
||||
$.info(`正在更新模块:${name}...`);
|
||||
|
||||
updateByName(allModules, name, newModule);
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res, newModule);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Module ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteModule(req, res) {
|
||||
let { name } = req.params;
|
||||
name = decodeURIComponent(name);
|
||||
$.info(`正在删除模块:${name}`);
|
||||
let allModules = $.read(MODULES_KEY);
|
||||
deleteByName(allModules, name);
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
function getAllModules(req, res) {
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
success(
|
||||
res,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
allModules.map(({ content, ...rest }) => rest),
|
||||
);
|
||||
}
|
||||
|
||||
function replaceModule(req, res) {
|
||||
const allModules = req.body;
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res);
|
||||
}
|
||||
56
backend/src/restful/node-info.js
Normal file
56
backend/src/restful/node-info.js
Normal 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}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,98 +12,172 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
async function compareSub(req, res) {
|
||||
const sub = req.body;
|
||||
const target = req.query.target || 'JSON';
|
||||
let content;
|
||||
if (sub.source === 'local') {
|
||||
content = sub.content;
|
||||
} else {
|
||||
try {
|
||||
content = await download(sub.url, sub.ua);
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new NetworkError(
|
||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
||||
'无法下载远程资源',
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
const original = ProxyUtils.parse(content);
|
||||
|
||||
// add id
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
sub.process || [],
|
||||
target,
|
||||
);
|
||||
|
||||
// produce
|
||||
success(res, { original, processed });
|
||||
}
|
||||
|
||||
async function compareCollection(req, res) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const collection = req.body;
|
||||
const subnames = collection.subscriptions;
|
||||
const results = {};
|
||||
|
||||
await Promise.all(
|
||||
subnames.map(async (name) => {
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
const sub = req.body;
|
||||
const target = req.query.target || 'JSON';
|
||||
let content;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
content = sub.content;
|
||||
} else {
|
||||
try {
|
||||
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 || [],
|
||||
'JSON',
|
||||
content = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'PROCESS_FAILED',
|
||||
`处理子订阅 ${name} 失败`,
|
||||
new NetworkError(
|
||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
||||
'无法下载远程资源',
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
content.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
content.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
const original = (Array.isArray(content) ? content : [content])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
// merge proxies with the original order
|
||||
const original = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name]),
|
||||
);
|
||||
// add id
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
proxy.subName = sub.name;
|
||||
});
|
||||
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
});
|
||||
// apply processors
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
sub.process || [],
|
||||
target,
|
||||
{ [sub.name]: sub },
|
||||
);
|
||||
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
collection.process || [],
|
||||
'JSON',
|
||||
);
|
||||
|
||||
success(res, { original, processed });
|
||||
// produce
|
||||
success(res, { original, processed });
|
||||
} catch (err) {
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`INTERNAL_SERVER_ERROR`,
|
||||
`Failed to preview subscription`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function compareCollection(req, res) {
|
||||
try {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const collection = req.body;
|
||||
const subnames = collection.subscriptions;
|
||||
const results = {};
|
||||
let hasError;
|
||||
await Promise.all(
|
||||
subnames.map(async (name) => {
|
||||
if (!hasError) {
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
let raw;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(
|
||||
sub.mergeSources,
|
||||
)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
);
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
raw.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
currentProxies.forEach((proxy) => {
|
||||
proxy.subName = sub.name;
|
||||
proxy.collectionName = collection.name;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
'JSON',
|
||||
{ [sub.name]: sub, _collection: collection },
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
} catch (err) {
|
||||
if (!hasError) {
|
||||
hasError = true;
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'PROCESS_FAILED',
|
||||
`处理子订阅 ${name} 失败`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (hasError) return;
|
||||
// merge proxies with the original order
|
||||
const original = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
proxy.collectionName = collection.name;
|
||||
});
|
||||
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
collection.process || [],
|
||||
'JSON',
|
||||
{ _collection: collection },
|
||||
);
|
||||
|
||||
success(res, { original, processed });
|
||||
} catch (err) {
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`INTERNAL_SERVER_ERROR`,
|
||||
`Failed to preview collection`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ export default function register($app) {
|
||||
}
|
||||
|
||||
async function getSettings(req, res) {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
let settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
|
||||
if (!settings.avatarUrl) await updateGitHubAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
success(res, settings);
|
||||
@@ -28,7 +33,7 @@ async function updateSettings(req, res) {
|
||||
success(res, newSettings);
|
||||
}
|
||||
|
||||
async function updateGitHubAvatar() {
|
||||
export async function updateGitHubAvatar() {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const username = settings.githubUser;
|
||||
if (username) {
|
||||
@@ -44,14 +49,18 @@ async function updateGitHubAvatar() {
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
settings.avatarUrl = data['avatar_url'];
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
} catch (e) {
|
||||
$.error('Failed to fetch GitHub avatar for User: ' + username);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateArtifactStore() {
|
||||
console.log('Updating artifact store');
|
||||
export async function updateArtifactStore() {
|
||||
$.log('Updating artifact store');
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const { githubUser, gistToken } = settings;
|
||||
if (githubUser && gistToken) {
|
||||
@@ -67,7 +76,11 @@ async function updateArtifactStore() {
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error('Failed to fetch artifact store for User: ' + githubUser);
|
||||
$.error(
|
||||
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function register($app) {
|
||||
function sortSubs(req, res) {
|
||||
const orders = req.body;
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
||||
allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res, allSubs);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
|
||||
function sortCollections(req, res) {
|
||||
const orders = req.body;
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
||||
allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res, allCols);
|
||||
}
|
||||
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
|
||||
function sortArtifacts(req, res) {
|
||||
const orders = req.body;
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
|
||||
allArtifacts.sort(
|
||||
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
|
||||
);
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
RequestInvalidError,
|
||||
} from './errors';
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { SUBS_KEY, COLLECTIONS_KEY } from '@/constants';
|
||||
import { getFlowHeaders } from '@/utils/flow';
|
||||
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
|
||||
import { success, failed } from './response';
|
||||
import $ from '@/core/app';
|
||||
|
||||
@@ -20,7 +20,10 @@ export default function register($app) {
|
||||
.patch(updateSubscription)
|
||||
.delete(deleteSubscription);
|
||||
|
||||
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
|
||||
$app.route('/api/subs')
|
||||
.get(getAllSubscriptions)
|
||||
.post(createSubscription)
|
||||
.put(replaceSubscriptions);
|
||||
}
|
||||
|
||||
// subscriptions API
|
||||
@@ -65,16 +68,7 @@ async function getFlowInfo(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
// unit is KB
|
||||
const upload = Number(flowHeaders.match(/upload=(\d+)/)[1]);
|
||||
const download = Number(flowHeaders.match(/download=(\d+)/)[1]);
|
||||
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
|
||||
|
||||
// optional expire timestamp
|
||||
const match = flowHeaders.match(/expire=(\d+)/);
|
||||
const expires = match ? Number(match[1]) : undefined;
|
||||
|
||||
success(res, { expires, total, usage: { upload, download } });
|
||||
success(res, parseFlowHeaders(flowHeaders));
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
@@ -89,6 +83,16 @@ async function getFlowInfo(req, res) {
|
||||
function createSubscription(req, res) {
|
||||
const sub = req.body;
|
||||
$.info(`正在创建订阅: ${sub.name}`);
|
||||
if (/\//.test(sub.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_NAME',
|
||||
`Subscription ${sub.name} is invalid`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
if (findByName(allSubs, sub.name)) {
|
||||
failed(
|
||||
@@ -98,6 +102,7 @@ function createSubscription(req, res) {
|
||||
`Subscription ${sub.name} already exists.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
allSubs.push(sub);
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
@@ -137,14 +142,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);
|
||||
@@ -184,3 +203,9 @@ function getAllSubscriptions(req, res) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
success(res, allSubs);
|
||||
}
|
||||
|
||||
function replaceSubscriptions(req, res) {
|
||||
const allSubs = req.body;
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
370
backend/src/restful/sync.js
Normal file
370
backend/src/restful/sync.js
Normal file
@@ -0,0 +1,370 @@
|
||||
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,
|
||||
url,
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
if (type === 'subscription') {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
let raw;
|
||||
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
||||
raw = content;
|
||||
} else if (url) {
|
||||
raw = await Promise.all(
|
||||
url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, ua)),
|
||||
);
|
||||
if (mergeSources === 'localFirst') {
|
||||
raw.unshift(content);
|
||||
} else if (mergeSources === 'remoteFirst') {
|
||||
raw.push(content);
|
||||
}
|
||||
} else if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
);
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
raw.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
let proxies = (Array.isArray(raw) ? raw : [raw])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
proxies.forEach((proxy) => {
|
||||
proxy.subName = sub.name;
|
||||
});
|
||||
// apply processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
sub.process || [],
|
||||
platform,
|
||||
{ [sub.name]: sub },
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`订阅 ${name} 中不含有效节点`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
if (exist[proxy.name]) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
`⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}!`,
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'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 = {};
|
||||
const errors = {};
|
||||
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' &&
|
||||
!['localFirst', 'remoteFirst'].includes(
|
||||
sub.mergeSources,
|
||||
)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
raw = await await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.map((url) => download(url, sub.ua)),
|
||||
);
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
raw.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
currentProxies.forEach((proxy) => {
|
||||
proxy.subName = sub.name;
|
||||
proxy.collectionName = collection.name;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
currentProxies = await ProxyUtils.process(
|
||||
currentProxies,
|
||||
sub.process || [],
|
||||
platform,
|
||||
{ [sub.name]: sub, _collection: collection },
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
processed++;
|
||||
$.info(
|
||||
`✅ 子订阅:${sub.name}加载成功,进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}% `,
|
||||
);
|
||||
} catch (err) {
|
||||
processed++;
|
||||
errors[name] = err;
|
||||
$.error(
|
||||
`❌ 处理组合订阅中的子订阅: ${
|
||||
sub.name
|
||||
}时出现错误:${err},该订阅已被跳过!进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}%`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
throw new Error(
|
||||
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
|
||||
// merge proxies with the original order
|
||||
let proxies = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
|
||||
proxies.forEach((proxy) => {
|
||||
proxy.collectionName = collection.name;
|
||||
});
|
||||
|
||||
// apply own processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
collection.process || [],
|
||||
platform,
|
||||
{ _collection: collection },
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
if (exist[proxy.name]) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
`⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}!`,
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'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;
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await produceArtifact({
|
||||
type: artifact.type,
|
||||
name: artifact.source,
|
||||
platform: artifact.platform,
|
||||
});
|
||||
|
||||
$.info(
|
||||
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
|
||||
artifact,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
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 };
|
||||
@@ -251,7 +251,7 @@ function createTestCases() {
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
alterId: 0,
|
||||
},
|
||||
},
|
||||
@@ -288,7 +288,7 @@ function createTestCases() {
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
alterId: 0,
|
||||
aead: true,
|
||||
},
|
||||
@@ -339,7 +339,7 @@ function createTestCases() {
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
@@ -402,7 +402,7 @@ function createTestCases() {
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'auto', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
|
||||
@@ -1,12 +1,55 @@
|
||||
import { HTTP } from '@/vendor/open-api';
|
||||
import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const cache = new Map();
|
||||
const tasks = new Map();
|
||||
|
||||
export default async function download(url, ua) {
|
||||
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
||||
const id = ua + url;
|
||||
if (cache.has(id)) {
|
||||
return cache.get(id);
|
||||
let $arguments = {};
|
||||
const rawArgs = url.split('#');
|
||||
if (rawArgs.length > 1) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
||||
} catch (e) {
|
||||
for (const pair of rawArgs[1].split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1];
|
||||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||
$arguments[key] =
|
||||
value == null || value === ''
|
||||
? true
|
||||
: decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
||||
if (downloadUrlMatch) {
|
||||
let type = downloadUrlMatch?.[1];
|
||||
let name = downloadUrlMatch?.[2];
|
||||
if (name == null) {
|
||||
throw new Error(`本地 ${type} URL 无效: ${url}`);
|
||||
}
|
||||
name = decodeURIComponent(name);
|
||||
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
||||
const item = findByName($.read(key), name);
|
||||
if (!item) {
|
||||
throw new Error(`找不到本地 ${type}: ${name}`);
|
||||
}
|
||||
|
||||
return item.content;
|
||||
}
|
||||
|
||||
const { isNode } = ENV();
|
||||
const { defaultUserAgent } = $.read(SETTINGS_KEY);
|
||||
ua = ua || defaultUserAgent || 'clash.meta';
|
||||
const id = hex_md5(ua + url);
|
||||
if (!isNode && tasks.has(id)) {
|
||||
return tasks.get(id);
|
||||
}
|
||||
|
||||
const http = HTTP({
|
||||
@@ -16,18 +59,30 @@ export default async function download(url, ua) {
|
||||
});
|
||||
|
||||
const result = new Promise((resolve, reject) => {
|
||||
http.get(url)
|
||||
.then((resp) => {
|
||||
const body = resp.body;
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('订阅内容为空!'));
|
||||
else resolve(body);
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error(`无法下载 URL:${url}`));
|
||||
});
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!$arguments?.noCache && cached) {
|
||||
resolve(cached);
|
||||
} else {
|
||||
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
|
||||
http.get(url)
|
||||
.then((resp) => {
|
||||
const body = resp.body;
|
||||
if (body.replace(/\s/g, '').length === 0)
|
||||
reject(new Error('远程资源内容为空!'));
|
||||
else {
|
||||
resourceCache.set(id, body);
|
||||
resolve(body);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error(`无法下载 URL:${url}`));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cache.set(id, result);
|
||||
if (!isNode) {
|
||||
tasks.set(id, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
16
backend/src/utils/env.js
Normal file
16
backend/src/utils/env.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { version as substoreVersion } from '../../package.json';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
|
||||
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';
|
||||
|
||||
export default {
|
||||
backend,
|
||||
version: substoreVersion,
|
||||
};
|
||||
@@ -3,7 +3,10 @@ import { HTTP } from '@/vendor/open-api';
|
||||
export async function getFlowHeaders(url) {
|
||||
const http = HTTP();
|
||||
const { headers } = await http.get({
|
||||
url,
|
||||
url: url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)[0],
|
||||
headers: {
|
||||
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
||||
},
|
||||
@@ -13,3 +16,28 @@ export async function getFlowHeaders(url) {
|
||||
)[0];
|
||||
return headers[subkey];
|
||||
}
|
||||
export function parseFlowHeaders(flowHeaders) {
|
||||
if (!flowHeaders) return;
|
||||
// unit is KB
|
||||
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
|
||||
const match = flowHeaders.match(/expire=(\d+)/);
|
||||
const expires = match ? Number(match[1]) : undefined;
|
||||
|
||||
return { expires, total, usage: { upload, download } };
|
||||
}
|
||||
export function flowTransfer(flow, unit = 'B') {
|
||||
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
|
||||
let unitIndex = unitList.indexOf(unit);
|
||||
|
||||
return flow < 1024
|
||||
? { value: flow.toFixed(1), unit: unit }
|
||||
: flowTransfer(flow / 1024, unitList[++unitIndex]);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function getFlag(name) {
|
||||
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
|
||||
'🇲🇹': ['Malta', '马耳他'],
|
||||
'🇲🇽': ['Mexico', '墨西哥'],
|
||||
'🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'],
|
||||
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
|
||||
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
|
||||
'🇳🇴': ['Norway', '挪威'],
|
||||
'🇳🇵': ['Nepal', '尼泊尔'],
|
||||
|
||||
@@ -40,6 +40,10 @@ export default class Gist {
|
||||
}
|
||||
|
||||
async upload(files) {
|
||||
if (Object.keys(files).length === 0) {
|
||||
return Promise.reject('未提供需上传的文件');
|
||||
}
|
||||
|
||||
const id = await this.locate();
|
||||
|
||||
if (id === -1) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
export function getPlatformFromHeaders(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let UA = '';
|
||||
let ua = '';
|
||||
for (let k of keys) {
|
||||
if (/USER-AGENT/i.test(k)) {
|
||||
UA = headers[k];
|
||||
ua = UA.toLowerCase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||
return 'QX';
|
||||
} else if (UA.indexOf('Surge Mac') !== -1) {
|
||||
return 'SurgeMac';
|
||||
} else if (UA.indexOf('Surge') !== -1) {
|
||||
return 'Surge';
|
||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||
return 'Loon';
|
||||
} else if (UA.indexOf('Shadowrocket') !== -1) {
|
||||
return 'Clash';
|
||||
return 'ShadowRocket';
|
||||
} else if (UA.indexOf('Stash') !== -1) {
|
||||
return 'Stash';
|
||||
} else if (
|
||||
ua === 'meta' ||
|
||||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
|
||||
) {
|
||||
return 'ClashMeta';
|
||||
} else if (ua.indexOf('clash') !== -1) {
|
||||
return 'Clash';
|
||||
} else if (ua.indexOf('v2ray') !== -1) {
|
||||
return 'V2Ray';
|
||||
} else {
|
||||
return 'JSON';
|
||||
}
|
||||
|
||||
56
backend/src/utils/resource-cache.js
Normal file
56
backend/src/utils/resource-cache.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import $ from '@/core/app';
|
||||
import { CACHE_EXPIRATION_TIME_MS, RESOURCE_CACHE_KEY } from '@/constants';
|
||||
|
||||
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 (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
});
|
||||
if (clear) this._persist();
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
$.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache(CACHE_EXPIRATION_TIME_MS);
|
||||
105
backend/src/utils/script-resource-cache.js
Normal file
105
backend/src/utils/script-resource-cache.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
SCRIPT_RESOURCE_CACHE_KEY,
|
||||
CSR_EXPIRATION_TIME_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
class ResourceCache {
|
||||
constructor() {
|
||||
this.expires = getExpiredTime();
|
||||
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
// clear obsolete cached resource
|
||||
let clear = false;
|
||||
Object.entries(this.resourceCache).forEach((entry) => {
|
||||
const [id, updated] = entry;
|
||||
if (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
});
|
||||
if (clear) this._persist();
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
gettime(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].time;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
function getExpiredTime() {
|
||||
// console.log($.read(CSR_EXPIRATION_TIME_KEY));
|
||||
if (!$.read(CSR_EXPIRATION_TIME_KEY)) {
|
||||
$.write('1728e5', CSR_EXPIRATION_TIME_KEY); // 48 * 3600 * 1000
|
||||
}
|
||||
let expiration = 1728e5;
|
||||
if ($.env.isLoon) {
|
||||
const loont = {
|
||||
// Loon 插件自义定
|
||||
'1\u5206\u949f': 6e4,
|
||||
'5\u5206\u949f': 3e5,
|
||||
'10\u5206\u949f': 6e5,
|
||||
'30\u5206\u949f': 18e5, // "30分钟"
|
||||
'1\u5c0f\u65f6': 36e5,
|
||||
'2\u5c0f\u65f6': 72e5,
|
||||
'3\u5c0f\u65f6': 108e5,
|
||||
'6\u5c0f\u65f6': 216e5,
|
||||
'12\u5c0f\u65f6': 432e5,
|
||||
'24\u5c0f\u65f6': 864e5,
|
||||
'48\u5c0f\u65f6': 1728e5,
|
||||
'72\u5c0f\u65f6': 2592e5, // "72小时"
|
||||
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
|
||||
};
|
||||
let intimed = $.read('#\u8282\u70b9\u7f13\u5b58\u6709\u6548\u671f'); // Loon #节点缓存有效期
|
||||
// console.log(intimed);
|
||||
if (intimed in loont) {
|
||||
expiration = loont[intimed];
|
||||
if (expiration === 'readcachets') {
|
||||
expiration = intimed;
|
||||
}
|
||||
}
|
||||
return expiration;
|
||||
} else {
|
||||
expiration = $.read(CSR_EXPIRATION_TIME_KEY);
|
||||
return expiration;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache();
|
||||
10
backend/src/vendor/express.js
vendored
10
backend/src/vendor/express.js
vendored
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { ENV } from './open-api';
|
||||
|
||||
export default function express({ substore: $, port }) {
|
||||
export default function express({ substore: $, port, host }) {
|
||||
port = port || 3000;
|
||||
host = host || '::';
|
||||
const { isNode } = ENV();
|
||||
const DEFAULT_HEADERS = {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
@@ -17,7 +18,7 @@ export default function express({ substore: $, port }) {
|
||||
const express_ = eval(`require("express")`);
|
||||
const bodyParser = eval(`require("body-parser")`);
|
||||
const app = express_();
|
||||
app.use(bodyParser.json({ verify: rawBodySaver }));
|
||||
app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
|
||||
app.use(
|
||||
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
|
||||
);
|
||||
@@ -29,8 +30,9 @@ export default function express({ substore: $, port }) {
|
||||
|
||||
// adapter
|
||||
app.start = () => {
|
||||
app.listen(port, () => {
|
||||
$.info(`Express started on port: ${port}`);
|
||||
const listener = app.listen(port, host, () => {
|
||||
const { address, port } = listener.address();
|
||||
$.info(`Express started on ${address}:${port}`);
|
||||
});
|
||||
};
|
||||
return app;
|
||||
|
||||
387
backend/src/vendor/md5.js
vendored
Normal file
387
backend/src/vendor/md5.js
vendored
Normal file
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||
* Digest Algorithm, as defined in RFC 1321.
|
||||
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
||||
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
||||
* Distributed under the BSD License
|
||||
* See http://pajhome.org.uk/crypt/md5 for more info.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Configurable variables. You may need to tweak these to be compatible with
|
||||
* the server-side, but the defaults work in most cases.
|
||||
*/
|
||||
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
|
||||
var b64pad = ''; /* base-64 pad character. "=" for strict RFC compliance */
|
||||
|
||||
/*
|
||||
* These are the functions you'll usually want to call
|
||||
* They take string arguments and return either hex or base-64 encoded strings
|
||||
*/
|
||||
export function hex_md5(s) {
|
||||
return rstr2hex(rstr_md5(str2rstr_utf8(s)));
|
||||
}
|
||||
|
||||
export function b64_md5(s) {
|
||||
return rstr2b64(rstr_md5(str2rstr_utf8(s)));
|
||||
}
|
||||
|
||||
export function any_md5(s, e) {
|
||||
return rstr2any(rstr_md5(str2rstr_utf8(s)), e);
|
||||
}
|
||||
|
||||
export function hex_hmac_md5(k, d) {
|
||||
return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
|
||||
}
|
||||
|
||||
export function b64_hmac_md5(k, d) {
|
||||
return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)));
|
||||
}
|
||||
|
||||
export function any_hmac_md5(k, d, e) {
|
||||
return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform a simple self-test to see if the VM is working
|
||||
*/
|
||||
function md5_vm_test() {
|
||||
return hex_md5('abc').toLowerCase() == '900150983cd24fb0d6963f7d28e17f72';
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the MD5 of a raw string
|
||||
*/
|
||||
function rstr_md5(s) {
|
||||
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the HMAC-MD5, of a key and some data (raw strings)
|
||||
*/
|
||||
function rstr_hmac_md5(key, data) {
|
||||
var bkey = rstr2binl(key);
|
||||
if (bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
|
||||
|
||||
var ipad = Array(16),
|
||||
opad = Array(16);
|
||||
for (var i = 0; i < 16; i++) {
|
||||
ipad[i] = bkey[i] ^ 0x36363636;
|
||||
opad[i] = bkey[i] ^ 0x5c5c5c5c;
|
||||
}
|
||||
|
||||
var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
|
||||
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a raw string to a hex string
|
||||
*/
|
||||
function rstr2hex(input) {
|
||||
try {
|
||||
hexcase;
|
||||
} catch (e) {
|
||||
hexcase = 0;
|
||||
}
|
||||
var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef';
|
||||
var output = '';
|
||||
var x;
|
||||
for (var i = 0; i < input.length; i++) {
|
||||
x = input.charCodeAt(i);
|
||||
output += hex_tab.charAt((x >>> 4) & 0x0f) + hex_tab.charAt(x & 0x0f);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a raw string to a base-64 string
|
||||
*/
|
||||
function rstr2b64(input) {
|
||||
try {
|
||||
b64pad;
|
||||
} catch (e) {
|
||||
b64pad = '';
|
||||
}
|
||||
var tab =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
var output = '';
|
||||
var len = input.length;
|
||||
for (var i = 0; i < len; i += 3) {
|
||||
var triplet =
|
||||
(input.charCodeAt(i) << 16) |
|
||||
(i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) |
|
||||
(i + 2 < len ? input.charCodeAt(i + 2) : 0);
|
||||
for (var j = 0; j < 4; j++) {
|
||||
if (i * 8 + j * 6 > input.length * 8) output += b64pad;
|
||||
else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3f);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a raw string to an arbitrary string encoding
|
||||
*/
|
||||
function rstr2any(input, encoding) {
|
||||
var divisor = encoding.length;
|
||||
var i, j, q, x, quotient;
|
||||
|
||||
/* Convert to an array of 16-bit big-endian values, forming the dividend */
|
||||
var dividend = Array(Math.ceil(input.length / 2));
|
||||
for (i = 0; i < dividend.length; i++) {
|
||||
dividend[i] =
|
||||
(input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Repeatedly perform a long division. The binary array forms the dividend,
|
||||
* the length of the encoding is the divisor. Once computed, the quotient
|
||||
* forms the dividend for the next step. All remainders are stored for later
|
||||
* use.
|
||||
*/
|
||||
var full_length = Math.ceil(
|
||||
(input.length * 8) / (Math.log(encoding.length) / Math.log(2)),
|
||||
);
|
||||
var remainders = Array(full_length);
|
||||
for (j = 0; j < full_length; j++) {
|
||||
quotient = Array();
|
||||
x = 0;
|
||||
for (i = 0; i < dividend.length; i++) {
|
||||
x = (x << 16) + dividend[i];
|
||||
q = Math.floor(x / divisor);
|
||||
x -= q * divisor;
|
||||
if (quotient.length > 0 || q > 0) quotient[quotient.length] = q;
|
||||
}
|
||||
remainders[j] = x;
|
||||
dividend = quotient;
|
||||
}
|
||||
|
||||
/* Convert the remainders to the output string */
|
||||
var output = '';
|
||||
for (i = remainders.length - 1; i >= 0; i--)
|
||||
output += encoding.charAt(remainders[i]);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Encode a string as utf-8.
|
||||
* For efficiency, this assumes the input is valid utf-16.
|
||||
*/
|
||||
function str2rstr_utf8(input) {
|
||||
var output = '';
|
||||
var i = -1;
|
||||
var x, y;
|
||||
|
||||
while (++i < input.length) {
|
||||
/* Decode utf-16 surrogate pairs */
|
||||
x = input.charCodeAt(i);
|
||||
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
|
||||
if (0xd800 <= x && x <= 0xdbff && 0xdc00 <= y && y <= 0xdfff) {
|
||||
x = 0x10000 + ((x & 0x03ff) << 10) + (y & 0x03ff);
|
||||
i++;
|
||||
}
|
||||
|
||||
/* Encode output as utf-8 */
|
||||
if (x <= 0x7f) output += String.fromCharCode(x);
|
||||
else if (x <= 0x7ff)
|
||||
output += String.fromCharCode(
|
||||
0xc0 | ((x >>> 6) & 0x1f),
|
||||
0x80 | (x & 0x3f),
|
||||
);
|
||||
else if (x <= 0xffff)
|
||||
output += String.fromCharCode(
|
||||
0xe0 | ((x >>> 12) & 0x0f),
|
||||
0x80 | ((x >>> 6) & 0x3f),
|
||||
0x80 | (x & 0x3f),
|
||||
);
|
||||
else if (x <= 0x1fffff)
|
||||
output += String.fromCharCode(
|
||||
0xf0 | ((x >>> 18) & 0x07),
|
||||
0x80 | ((x >>> 12) & 0x3f),
|
||||
0x80 | ((x >>> 6) & 0x3f),
|
||||
0x80 | (x & 0x3f),
|
||||
);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Encode a string as utf-16
|
||||
*/
|
||||
function str2rstr_utf16le(input) {
|
||||
var output = '';
|
||||
for (var i = 0; i < input.length; i++)
|
||||
output += String.fromCharCode(
|
||||
input.charCodeAt(i) & 0xff,
|
||||
(input.charCodeAt(i) >>> 8) & 0xff,
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
function str2rstr_utf16be(input) {
|
||||
var output = '';
|
||||
for (var i = 0; i < input.length; i++)
|
||||
output += String.fromCharCode(
|
||||
(input.charCodeAt(i) >>> 8) & 0xff,
|
||||
input.charCodeAt(i) & 0xff,
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert a raw string to an array of little-endian words
|
||||
* Characters >255 have their high-byte silently ignored.
|
||||
*/
|
||||
function rstr2binl(input) {
|
||||
var output = Array(input.length >> 2);
|
||||
for (var i = 0; i < output.length; i++) output[i] = 0;
|
||||
for (var i = 0; i < input.length * 8; i += 8)
|
||||
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert an array of little-endian words to a string
|
||||
*/
|
||||
function binl2rstr(input) {
|
||||
var output = '';
|
||||
for (var i = 0; i < input.length * 32; i += 8)
|
||||
output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the MD5 of an array of little-endian words, and a bit length.
|
||||
*/
|
||||
function binl_md5(x, len) {
|
||||
/* append padding */
|
||||
x[len >> 5] |= 0x80 << len % 32;
|
||||
x[(((len + 64) >>> 9) << 4) + 14] = len;
|
||||
|
||||
var a = 1732584193;
|
||||
var b = -271733879;
|
||||
var c = -1732584194;
|
||||
var d = 271733878;
|
||||
|
||||
for (var i = 0; i < x.length; i += 16) {
|
||||
var olda = a;
|
||||
var oldb = b;
|
||||
var oldc = c;
|
||||
var oldd = d;
|
||||
|
||||
a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
|
||||
d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
|
||||
c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
|
||||
b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
|
||||
a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
|
||||
d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
|
||||
c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
|
||||
b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
|
||||
a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
|
||||
d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
|
||||
c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
|
||||
b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
|
||||
a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
|
||||
d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
|
||||
c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
|
||||
b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
|
||||
|
||||
a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
|
||||
d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
|
||||
c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
|
||||
b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
|
||||
a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
|
||||
d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
|
||||
c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
|
||||
b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
|
||||
a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
|
||||
d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
|
||||
c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
|
||||
b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
|
||||
a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
|
||||
d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
|
||||
c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
|
||||
b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
|
||||
|
||||
a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
|
||||
d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
|
||||
c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
|
||||
b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
|
||||
a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
|
||||
d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
|
||||
c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
|
||||
b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
|
||||
a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
|
||||
d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
|
||||
c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
|
||||
b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
|
||||
a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
|
||||
d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
|
||||
c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
|
||||
b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
|
||||
|
||||
a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
|
||||
d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
|
||||
c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
|
||||
b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
|
||||
a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
|
||||
d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
|
||||
c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
|
||||
b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
|
||||
a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
|
||||
d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
|
||||
c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
|
||||
b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
|
||||
a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
|
||||
d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
|
||||
c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
|
||||
b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
|
||||
|
||||
a = safe_add(a, olda);
|
||||
b = safe_add(b, oldb);
|
||||
c = safe_add(c, oldc);
|
||||
d = safe_add(d, oldd);
|
||||
}
|
||||
return Array(a, b, c, d);
|
||||
}
|
||||
|
||||
/*
|
||||
* These functions implement the four basic operations the algorithm uses.
|
||||
*/
|
||||
function md5_cmn(q, a, b, x, s, t) {
|
||||
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
|
||||
}
|
||||
|
||||
function md5_ff(a, b, c, d, x, s, t) {
|
||||
return md5_cmn((b & c) | (~b & d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md5_gg(a, b, c, d, x, s, t) {
|
||||
return md5_cmn((b & d) | (c & ~d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md5_hh(a, b, c, d, x, s, t) {
|
||||
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md5_ii(a, b, c, d, x, s, t) {
|
||||
return md5_cmn(c ^ (b | ~d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||
* to work around bugs in some JS interpreters.
|
||||
*/
|
||||
function safe_add(x, y) {
|
||||
var lsw = (x & 0xffff) + (y & 0xffff);
|
||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xffff);
|
||||
}
|
||||
|
||||
/*
|
||||
* Bitwise rotate a 32-bit number to the left.
|
||||
*/
|
||||
function bit_rol(num, cnt) {
|
||||
return (num << cnt) | (num >>> (32 - cnt));
|
||||
}
|
||||
59
backend/src/vendor/open-api.js
vendored
59
backend/src/vendor/open-api.js
vendored
@@ -49,31 +49,32 @@ export class OpenAPI {
|
||||
|
||||
if (isNode) {
|
||||
// create a json for root cache
|
||||
let fpath = 'root.json';
|
||||
if (!this.node.fs.existsSync(fpath)) {
|
||||
this.node.fs.writeFileSync(
|
||||
fpath,
|
||||
JSON.stringify({}),
|
||||
{ flag: 'wx' },
|
||||
(err) => console.log(err),
|
||||
const basePath =
|
||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||
let rootPath = `${basePath}/root.json`;
|
||||
|
||||
this.log(`Root path: ${rootPath}`);
|
||||
if (!this.node.fs.existsSync(rootPath)) {
|
||||
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
|
||||
flag: 'wx',
|
||||
});
|
||||
this.root = {};
|
||||
} else {
|
||||
this.root = JSON.parse(
|
||||
this.node.fs.readFileSync(`${rootPath}`),
|
||||
);
|
||||
}
|
||||
this.root = {};
|
||||
|
||||
// create a json file with the given name if not exists
|
||||
fpath = `${this.name}.json`;
|
||||
let fpath = `${basePath}/${this.name}.json`;
|
||||
this.log(`Data path: ${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, JSON.stringify({}), {
|
||||
flag: 'wx',
|
||||
});
|
||||
this.cache = {};
|
||||
} else {
|
||||
this.cache = JSON.parse(
|
||||
this.node.fs.readFileSync(`${this.name}.json`),
|
||||
);
|
||||
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,14 +85,17 @@ export class OpenAPI {
|
||||
if (isQX) $prefs.setValueForKey(data, this.name);
|
||||
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
||||
if (isNode) {
|
||||
const basePath =
|
||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||
|
||||
this.node.fs.writeFileSync(
|
||||
`${this.name}.json`,
|
||||
`${basePath}/${this.name}.json`,
|
||||
data,
|
||||
{ flag: 'w' },
|
||||
(err) => console.log(err),
|
||||
);
|
||||
this.node.fs.writeFileSync(
|
||||
'root.json',
|
||||
`${basePath}/root.json`,
|
||||
JSON.stringify(this.root, null, 2),
|
||||
{ flag: 'w' },
|
||||
(err) => console.log(err),
|
||||
@@ -257,6 +261,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({
|
||||
@@ -301,7 +316,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||
return (
|
||||
timer
|
||||
? Promise.race([timer, worker]).then((res) => {
|
||||
clearTimeout(timeoutid);
|
||||
if (typeof clearTimeout !== 'undefined') {
|
||||
clearTimeout(timeoutid);
|
||||
}
|
||||
return res;
|
||||
})
|
||||
: worker
|
||||
|
||||
16
backend/sub-store.min.js
vendored
16
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -2,11 +2,18 @@
|
||||
#!desc=高级订阅管理工具
|
||||
#!openUrl=https://sub.store
|
||||
#!author=Peng-YM
|
||||
#!homepage=https://github.com/Peng-YM/Sub-Store
|
||||
#!homepage=https://github.com/sub-store-org/Sub-Store
|
||||
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
||||
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||
|
||||
[Rule]
|
||||
DOMAIN,sub-store.vercel.app,PROXY
|
||||
|
||||
[MITM]
|
||||
hostname=sub.store
|
||||
|
||||
[Script]
|
||||
http-request https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/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
|
||||
7
config/QX-Task.json
Normal file
7
config/QX-Task.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name":"Sub-Store",
|
||||
"description":"",
|
||||
"task":[
|
||||
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
hostname=sub.store
|
||||
|
||||
^https?:\/\/sub\.store url script-analyze-echo-response https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/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
|
||||
@@ -1,20 +1,32 @@
|
||||
# Sub-Store 配置指南
|
||||
|
||||
## 查看更新说明:
|
||||
|
||||
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
|
||||
|
||||
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||
|
||||
## 脚本配置:
|
||||
|
||||
### 1. Loon
|
||||
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。
|
||||
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
|
||||
|
||||
### 2. Surge
|
||||
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
|
||||
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
|
||||
|
||||
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
|
||||
|
||||
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
|
||||
|
||||
|
||||
### 3. QX
|
||||
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可
|
||||
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可。
|
||||
|
||||
### 4. Stash
|
||||
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。
|
||||
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
|
||||
|
||||
### 5. Shadowrocket
|
||||
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
|
||||
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
|
||||
|
||||
## 使用 Sub-Store
|
||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||
|
||||
@@ -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://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/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
|
||||
12
config/Surge-Noability.sgmodule
Normal file
12
config/Surge-Noability.sgmodule
Normal file
@@ -0,0 +1,12 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 则可以使用此脚本
|
||||
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
|
||||
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
|
||||
11
config/Surge-ability.sgmodule
Normal file
11
config/Surge-ability.sgmodule
Normal file
@@ -0,0 +1,11 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
|
||||
|
||||
[MITM]
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
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
|
||||
@@ -1,7 +1,11 @@
|
||||
#!name=Sub-Store
|
||||
#!desc=高级订阅管理工具 @Peng-YM
|
||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
||||
|
||||
[MITM]
|
||||
hostname=%APPEND% sub.store
|
||||
hostname = %APPEND% sub.store
|
||||
|
||||
[Script]
|
||||
Sub-Store = type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/backend/sub-store.min.js,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
|
||||
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
|
||||
|
||||
175
scripts/ip-flag.js
Normal file
175
scripts/ip-flag.js
Normal file
@@ -0,0 +1,175 @@
|
||||
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 (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
});
|
||||
if (clear) this._persist();
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
$.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value }
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
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 - _ }
|
||||
0
scripts/media-filter.js
Normal file
0
scripts/media-filter.js
Normal file
@@ -1,11 +1,5 @@
|
||||
const $ = API("sub-store");
|
||||
$.write({}, "subs");
|
||||
$.write({}, "collections");
|
||||
$.write({}, "artifacts");
|
||||
const $ = API()
|
||||
$.write("{}", "#sub-store")
|
||||
$.done()
|
||||
|
||||
$.done();
|
||||
|
||||
// prettier-ignore
|
||||
/*********************************** API *************************************/
|
||||
function ENV(){const e="undefined"!=typeof $task,t="undefined"!=typeof $loon,s="undefined"!=typeof $httpClient&&!t,o="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:"function"==typeof require&&!o,isJSBox:o,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e,t={}){const{isQX:s,isLoon:o,isSurge:i,isScriptable:n,isNode:r}=ENV();const u={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(c=>u[c.toLowerCase()]=(u=>(function(u,c){(c="string"==typeof c?{url:c}:c).url=e?e+c.url:c.url;const h=(c={...t,...c}).timeout,l={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...c.events};let a,d;if(l.onRequest(u,c),s)a=$task.fetch({method:u,...c});else if(o||i||r)a=new Promise((e,t)=>{(r?require("request"):$httpClient)[u.toLowerCase()](c,(s,o,i)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:i})})});else if(n){const e=new Request(c.url);e.method=u,e.headers=c.headers,e.body=c.body,a=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}const f=h?new Promise((e,t)=>{d=setTimeout(()=>(l.onTimeout(),t(`${u} URL: ${c.url} exceeds the timeout ${h} ms`)),h)}):null;return(f?Promise.race([f,a]).then(e=>(clearTimeout(d),e)):a).then(e=>l.onResponse(e))})(c,u))),u}function API(e="untitled",t=!1){const{isQX:s,isLoon:o,isSurge:i,isNode:n,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(n){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(o||i)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),n){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache);s&&$prefs.setValueForKey(e,this.name),(o||i)&&$persistentStore.write(e,this.name),n&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root),{flag:"w"},e=>console.log(e)))}write(e,t){this.log(`SET ${t}`),-1!==t.indexOf("#")?(t=t.substr(1),i&o&&$persistentStore.write(e,t),s&&$prefs.setValueForKey(e,t),n&&(this.root[t]=e)):this.cache[t]=e,this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),i&o?$persistentStore.read(e):s?$prefs.valueForKey(e):n?this.root[e]:void 0)}delete(e){this.log(`DELETE ${e}`),-1!==e.indexOf("#")?(e=e.substr(1),i&o&&$persistentStore.write(null,e),s&&$prefs.removeValueForKey(e),n&&delete this.root[e]):delete this.cache[e],this.persistCache()}notify(e,t="",c="",h={}){const l=h["open-url"],a=h["media-url"];if(s&&$notify(e,t,c,h),i&&$notification.post(e,t,c+`${a?"\n多媒体:"+a:""}`,{url:l}),o){let s={};l&&(s.openUrl=l),a&&(s.mediaUrl=a),"{}"==JSON.stringify(s)?$notification.post(e,t,c):$notification.post(e,t,c,s)}if(n||u){const s=c+(l?`\n点击跳转: ${l}`:"")+(a?`\n多媒体: ${a}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(e)}info(e){console.log(e)}error(e){console.log("ERROR: "+e)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||i?$done(e):n&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}}(e,t)}
|
||||
/*****************************************************************************/
|
||||
function ENV(){const e="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:"undefined"!=typeof $task,isLoon:"undefined"!=typeof $loon,isSurge:"undefined"!=typeof $httpClient&&"undefined"!=typeof $utils,isBrowser:"undefined"!=typeof document,isNode:"function"==typeof require&&!e,isJSBox:e,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:o,isScriptable:n,isNode:i,isBrowser:r}=ENV(),u=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;const a={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(h=>a[h.toLowerCase()]=(a=>(function(a,h){h="string"==typeof h?{url:h}:h;const d=e.baseURL;d&&!u.test(h.url||"")&&(h.url=d?d+h.url:h.url),h.body&&h.headers&&!h.headers["Content-Type"]&&(h.headers["Content-Type"]="application/x-www-form-urlencoded");const l=(h={...e,...h}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...h.events};let f,p;if(c.onRequest(a,h),t)f=$task.fetch({method:a,...h});else if(s||o||i)f=new Promise((e,t)=>{(i?require("request"):$httpClient)[a.toLowerCase()](h,(s,o,n)=>{s?t(s):e({statusCode:o.status||o.statusCode,headers:o.headers,body:n})})});else if(n){const e=new Request(h.url);e.method=a,e.headers=h.headers,e.body=h.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}else r&&(f=new Promise((e,t)=>{fetch(h.url,{method:a,headers:h.headers,body:h.body}).then(e=>e.json()).then(t=>e({statusCode:t.status,headers:t.headers,body:t.data})).catch(t)}));const y=l?new Promise((e,t)=>{p=setTimeout(()=>(c.onTimeout(),t(`${a} URL: ${h.url} exceeds the timeout ${l} ms`)),l)}):null;return(y?Promise.race([y,f]).then(e=>(clearTimeout(p),e)):f).then(e=>c.onResponse(e))})(h,a))),a}function API(e="untitled",t=!1){const{isQX:s,isLoon:o,isSurge:n,isNode:i,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(i){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(o||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),i){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(o||n)&&$persistentStore.write(e,this.name),i&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||o)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);i&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||o?$persistentStore.read(e):s?$prefs.valueForKey(e):i?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||o)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);i&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",a="",h={}){const d=h["open-url"],l=h["media-url"];if(s&&$notify(e,t,a,h),n&&$notification.post(e,t,a+`${l?"\n多媒体:"+l:""}`,{url:d}),o){let s={};d&&(s.openUrl=d),l&&(s.mediaUrl=l),"{}"===JSON.stringify(s)?$notification.post(e,t,a):$notification.post(e,t,a,s)}if(i||u){const s=a+(d?`\n点击跳转: ${d}`:"")+(l?`\n多媒体: ${l}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||o||n?$done(e):i&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)}
|
||||
|
||||
Reference in New Issue
Block a user