mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
470 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce8794463d | ||
|
|
9426f128c4 | ||
|
|
ebc7173c95 | ||
|
|
dd4e0cef68 | ||
|
|
b1618c3803 | ||
|
|
1b4c046b75 | ||
|
|
41034ceb46 | ||
|
|
6efb19c856 | ||
|
|
2cd30dfe68 | ||
|
|
d53947d820 | ||
|
|
7e75031e92 | ||
|
|
4a07c02dc1 | ||
|
|
95d6688539 | ||
|
|
a23e2ffcd6 | ||
|
|
fda1252d0e | ||
|
|
62c5c2e15b | ||
|
|
ffabcc9391 | ||
|
|
0825f15d04 | ||
|
|
fbf6b5ce6e | ||
|
|
3eb0816c88 | ||
|
|
8fc755ff02 | ||
|
|
6d3d6fa1b3 | ||
|
|
4ef4431c2c | ||
|
|
5058662651 | ||
|
|
f9d120bac3 | ||
|
|
72a445ae33 | ||
|
|
5e2a87e250 | ||
|
|
71fc9affbf | ||
|
|
6f82294c49 | ||
|
|
7c398ba51c | ||
|
|
7002eee88d | ||
|
|
bd21d58fe7 | ||
|
|
2ea46dcbf1 | ||
|
|
4a2a2297f6 | ||
|
|
07d5a913f0 | ||
|
|
421df8f0d4 | ||
|
|
e14944dd19 | ||
|
|
bf18c51f6a | ||
|
|
23e8fbd1b7 | ||
|
|
b94b3c366b | ||
|
|
afb5f7b880 | ||
|
|
74ec133a79 | ||
|
|
2a76eb6462 | ||
|
|
9ac5e136a6 | ||
|
|
38f5a97a20 | ||
|
|
14a3488ce2 | ||
|
|
6afec4f668 | ||
|
|
b1874e510d | ||
|
|
48aaaf5c99 | ||
|
|
7385e17a4c | ||
|
|
c3daea55ab | ||
|
|
fc9ff48b1f | ||
|
|
fb21890b68 | ||
|
|
2155cc9639 | ||
|
|
03e320cbd0 | ||
|
|
e325b9a39a | ||
|
|
87597f6fc2 | ||
|
|
3462d36c35 | ||
|
|
02946ec81c | ||
|
|
c963c872ff | ||
|
|
c4a1bb4ea1 | ||
|
|
f96d9dea74 | ||
|
|
01eb69d8ae | ||
|
|
797ba6f601 | ||
|
|
128353a7f3 | ||
|
|
e6f6d51608 | ||
|
|
589a6bfadb | ||
|
|
75012503f8 | ||
|
|
85a3e2ee54 | ||
|
|
95b7557635 | ||
|
|
14ca62db4a | ||
|
|
a2a754adb7 | ||
|
|
6b23f82953 | ||
|
|
e071a7f253 | ||
|
|
b9bba895e1 | ||
|
|
8090d678ee | ||
|
|
ff4be7ac38 | ||
|
|
7e2109dc68 | ||
|
|
278beae99a | ||
|
|
3aedd5943d | ||
|
|
222551eb20 | ||
|
|
0d5e1ab38b | ||
|
|
a3ec98caa9 | ||
|
|
d9e4d814bb | ||
|
|
e843aa3702 | ||
|
|
66464645f2 | ||
|
|
9ccd6b3816 | ||
|
|
74be1e3d82 | ||
|
|
6d78eb7356 | ||
|
|
38eccca8b4 | ||
|
|
33e5aeceb5 | ||
|
|
837667edc9 | ||
|
|
0069b0ce83 | ||
|
|
fcc9d047ae | ||
|
|
382d22e622 | ||
|
|
06f3e97af2 | ||
|
|
bd87e9231e | ||
|
|
d1d6d19542 | ||
|
|
08bf0b78bb | ||
|
|
9a3cd4f57c | ||
|
|
d015c7867e | ||
|
|
4713b63083 | ||
|
|
dbf9e7c360 | ||
|
|
4ea84118c4 | ||
|
|
dda8113a42 | ||
|
|
f16b2d34f1 | ||
|
|
5b28e1a4c9 | ||
|
|
8d0a71d983 | ||
|
|
815552d470 | ||
|
|
9d90369594 | ||
|
|
6aece471aa | ||
|
|
99396773f6 | ||
|
|
e229408a2d | ||
|
|
514414587b | ||
|
|
d4c419745e | ||
|
|
fe3da254f4 | ||
|
|
7d8132d7cd | ||
|
|
bc1247efaf | ||
|
|
dea937df66 | ||
|
|
cfb5a8e082 | ||
|
|
4790bf47d1 | ||
|
|
56fd495fb6 | ||
|
|
f4639d9a34 | ||
|
|
cc58a5541e | ||
|
|
772f431887 | ||
|
|
2b60c515cd | ||
|
|
c8c22c3901 | ||
|
|
d8f9466b84 | ||
|
|
d12ccad382 | ||
|
|
b4358663cc | ||
|
|
aba6264988 | ||
|
|
2320ab3838 | ||
|
|
542957d34a | ||
|
|
07e50175f9 | ||
|
|
e09d66060d | ||
|
|
b048ecdfff | ||
|
|
aac72fb9a3 | ||
|
|
baec193e5c | ||
|
|
8fe818f826 | ||
|
|
72286984ec | ||
|
|
27e693c308 | ||
|
|
6cf8080cd3 | ||
|
|
839fcacf63 | ||
|
|
a2e45bcb10 | ||
|
|
ea0eb91691 | ||
|
|
1f0ddf2d28 | ||
|
|
a660c6ff90 | ||
|
|
71d9adbc07 | ||
|
|
97bec9183a | ||
|
|
ef85b6d0e9 | ||
|
|
8ffb060cb4 | ||
|
|
6d43961e96 | ||
|
|
f3200aea8c | ||
|
|
e2346d16a2 | ||
|
|
dc320eaa6c | ||
|
|
02031019f7 | ||
|
|
5d09fe782f | ||
|
|
6e425e5908 | ||
|
|
d10c9233c0 | ||
|
|
cc556b641d | ||
|
|
de2813b035 | ||
|
|
c9158ceb1d | ||
|
|
5cf0c98f5f | ||
|
|
7d0414f8ca | ||
|
|
bee1d62a1a | ||
|
|
72bc9b9456 | ||
|
|
3b4c14e7d0 | ||
|
|
59d93483bb | ||
|
|
75d88c02c7 | ||
|
|
99d5868cff | ||
|
|
e1489a3cf7 | ||
|
|
59fe16a7b0 | ||
|
|
562d349629 | ||
|
|
9ce14351c5 | ||
|
|
76e781c711 | ||
|
|
f0acf4a2a7 | ||
|
|
9abeb4ce7b | ||
|
|
153802c7c4 | ||
|
|
19418b631f | ||
|
|
97caeed208 | ||
|
|
dd8d1d85e8 | ||
|
|
14ed56b5d5 | ||
|
|
9785271c5b | ||
|
|
05bdf95a29 | ||
|
|
317a804b36 | ||
|
|
10ec8a25a2 | ||
|
|
aa0943a909 | ||
|
|
a0c1bbbf70 | ||
|
|
fea9de4fae | ||
|
|
cddd1818fe | ||
|
|
f94830b2df | ||
|
|
e02a26040e | ||
|
|
6906efdd55 | ||
|
|
9558b63261 | ||
|
|
4bfdef17ee | ||
|
|
9d29fc8a09 | ||
|
|
f524920c13 | ||
|
|
bfe072cbdf | ||
|
|
32dcca4a26 | ||
|
|
a5d77c39c8 | ||
|
|
6ea1b69a62 | ||
|
|
2b3b9177e5 | ||
|
|
91aab3ca7a | ||
|
|
c1a9fc6abc | ||
|
|
11d9ce7372 | ||
|
|
ad3d2270ac | ||
|
|
3ad42f2c10 | ||
|
|
ec06eb8659 | ||
|
|
4a23a4d8b6 | ||
|
|
913638a233 | ||
|
|
bf642ce0e6 | ||
|
|
1ecac9da92 | ||
|
|
c5a417da8f | ||
|
|
8cd0545023 | ||
|
|
b6f848a6e6 | ||
|
|
99d058bcf1 | ||
|
|
533103e765 | ||
|
|
cf82764171 | ||
|
|
7b783c1fe3 | ||
|
|
372eff9a44 | ||
|
|
d3b5a529d7 | ||
|
|
8049134bb5 | ||
|
|
3f620700a4 | ||
|
|
9e64a68481 | ||
|
|
9ce5916414 | ||
|
|
047c21fe70 | ||
|
|
47849dc6d0 | ||
|
|
af06086c1b | ||
|
|
4a6a147667 | ||
|
|
c6540d14cd | ||
|
|
3db71ec531 | ||
|
|
cf156c2f17 | ||
|
|
e28e2a78fb | ||
|
|
b0a2c709e8 | ||
|
|
5dc2c8ced7 | ||
|
|
d2a65ee0fe | ||
|
|
4dd4ae98ca | ||
|
|
0d41eb467f | ||
|
|
ba1c91a7a5 | ||
|
|
30fa87c172 | ||
|
|
1eaa33948b | ||
|
|
619e256ed8 | ||
|
|
b46209e164 | ||
|
|
a1ba4e273e | ||
|
|
bfc95ed92a | ||
|
|
32f591ec56 | ||
|
|
cea16d8c44 | ||
|
|
93a1ba7b50 | ||
|
|
e6d1aa1150 | ||
|
|
26e83798da | ||
|
|
b083d2d840 | ||
|
|
cf35afcab2 | ||
|
|
d073dfeef8 | ||
|
|
f970ea3361 | ||
|
|
630bac0575 | ||
|
|
7f3cb2b191 | ||
|
|
92e1e4a0fb | ||
|
|
3b85d313fe | ||
|
|
c91d8e28e4 | ||
|
|
8cbb4492be | ||
|
|
6f7da57e3a | ||
|
|
2586c29746 | ||
|
|
6f7fe8204b | ||
|
|
bafaf07743 | ||
|
|
9962eb0947 | ||
|
|
ac5232a7bc | ||
|
|
2301ccbfb5 | ||
|
|
0b5761e5fc | ||
|
|
3ab21b0e26 | ||
|
|
89ab72e46c | ||
|
|
18bd6526d0 | ||
|
|
c7329c32eb | ||
|
|
4819ae95e4 | ||
|
|
370d228b04 | ||
|
|
d092916168 | ||
|
|
0c93de48ab | ||
|
|
274aa50373 | ||
|
|
e24de8d0b6 | ||
|
|
93a5ce6c3b | ||
|
|
cb66c8daa2 | ||
|
|
f4cdc953e6 | ||
|
|
2a1c2eb9df | ||
|
|
6217c2e5cd | ||
|
|
f90d9c2fd1 | ||
|
|
3e952e9e88 | ||
|
|
a81b55f752 | ||
|
|
33652af516 | ||
|
|
2bca669930 | ||
|
|
f1bf0e1e8d | ||
|
|
16b9cd9aaf | ||
|
|
32eb069ab2 | ||
|
|
4c9f8011c7 | ||
|
|
bd26b0a561 | ||
|
|
958d1e52c8 | ||
|
|
e7a2e60963 | ||
|
|
fa6a274f79 | ||
|
|
e40b3f88d5 | ||
|
|
163ad9ee09 | ||
|
|
abb6f2dec1 | ||
|
|
56870bbd5f | ||
|
|
efbc6ecd84 | ||
|
|
c27c589024 | ||
|
|
0efed4f1a0 | ||
|
|
e3a514d1fb | ||
|
|
64478c7a27 | ||
|
|
dc8f19f350 | ||
|
|
b4ccfc7e07 | ||
|
|
3f1940630a | ||
|
|
5a0bdb1276 | ||
|
|
a1b86e26a2 | ||
|
|
6ec8c29f6a | ||
|
|
bbb9602f9f | ||
|
|
6db6153672 | ||
|
|
b66189948a | ||
|
|
2611dccc73 | ||
|
|
25d3cf6ca4 | ||
|
|
3637c5eb74 | ||
|
|
80d46597b4 | ||
|
|
ca65e4209e | ||
|
|
53bb4866e7 | ||
|
|
09495fa607 | ||
|
|
4b27d40602 | ||
|
|
518de2e919 | ||
|
|
078bf228de | ||
|
|
aaef97cf5d | ||
|
|
7beff4013f | ||
|
|
23cf81d0a5 | ||
|
|
572f2f5533 | ||
|
|
1c6d761e09 | ||
|
|
437297b8b0 | ||
|
|
ca437865e6 | ||
|
|
739100c873 | ||
|
|
a4384f4f13 | ||
|
|
468d136f0e | ||
|
|
b0c1157fe1 | ||
|
|
56626dabc7 | ||
|
|
2a87f7b3c3 | ||
|
|
81adfbc461 | ||
|
|
e04217c50d | ||
|
|
391a5aa2e4 | ||
|
|
2f3b42f552 | ||
|
|
76302f9d53 | ||
|
|
1924e9735c | ||
|
|
6a8cee3cd5 | ||
|
|
9b2209cc8b | ||
|
|
c585785c50 | ||
|
|
c3e5da7ee4 | ||
|
|
2ecad8bbb8 | ||
|
|
a642213928 | ||
|
|
f85e360ea8 | ||
|
|
1b948cdf52 | ||
|
|
556d5f393c | ||
|
|
a8c05207c0 | ||
|
|
e97fb1e6d9 | ||
|
|
e40b9a77c4 | ||
|
|
df0ac8a218 | ||
|
|
36377f3c20 | ||
|
|
47f26bdac8 | ||
|
|
a8a89ee2a2 | ||
|
|
199c5fb337 | ||
|
|
df505616ec | ||
|
|
c5b11f8b36 | ||
|
|
b19b49d2fa | ||
|
|
395c6e4e4a | ||
|
|
ae1c738f70 | ||
|
|
02d54208b0 | ||
|
|
5d3fc499ce | ||
|
|
d23bc7663e | ||
|
|
15704ea1c9 | ||
|
|
68cb393a7e | ||
|
|
2e99f28aa5 | ||
|
|
1a18e65e47 | ||
|
|
bbd5341d7a | ||
|
|
623802d73f | ||
|
|
19920dbfa3 | ||
|
|
8764e01d7e | ||
|
|
31b6dd0507 | ||
|
|
a84007d39e | ||
|
|
751e50bf99 | ||
|
|
a91f978042 | ||
|
|
1248e6b32a | ||
|
|
d0f255d9c6 | ||
|
|
060415584e | ||
|
|
8a2087c53a | ||
|
|
64117c50c7 | ||
|
|
6564d9497a | ||
|
|
1c03e46bbb | ||
|
|
084b385fdb | ||
|
|
290b9b5411 | ||
|
|
e5c1ae9ed8 | ||
|
|
9b6d9d49f9 | ||
|
|
a12adf5255 | ||
|
|
8682f14ee7 | ||
|
|
b3de7a4bc5 | ||
|
|
099ae5ad83 | ||
|
|
c7d00ac512 | ||
|
|
ca0d800bbb | ||
|
|
31b48d7a6c | ||
|
|
ab96ae9413 | ||
|
|
3fc507b576 | ||
|
|
2f2dbbdb68 | ||
|
|
1543e76841 | ||
|
|
74c4719806 | ||
|
|
b80d7f5875 | ||
|
|
779950ab11 | ||
|
|
42404537e8 | ||
|
|
228566116d | ||
|
|
9bb06bf438 | ||
|
|
88e52f9787 | ||
|
|
845a173738 | ||
|
|
4a6bcbc9b4 | ||
|
|
bbaac2de6f | ||
|
|
614438ae3d | ||
|
|
4966132397 | ||
|
|
059c4bd148 | ||
|
|
63887e3dad | ||
|
|
7fd585b5d4 | ||
|
|
16c79ac0fc | ||
|
|
14d9885db8 | ||
|
|
1e61088ed8 | ||
|
|
af6904ea50 | ||
|
|
1bc44ccde8 | ||
|
|
bdc7ee50f7 | ||
|
|
812f24d102 | ||
|
|
8c943176a5 | ||
|
|
f4c4cdba67 | ||
|
|
ada03be05f | ||
|
|
5584225413 | ||
|
|
5cbcf4fce4 | ||
|
|
89931c0032 | ||
|
|
88f3198320 | ||
|
|
27a14bb255 | ||
|
|
5ecce27f4e | ||
|
|
12903d77f7 | ||
|
|
0c6ec7f82a | ||
|
|
ba251ced34 | ||
|
|
d96a0421f7 | ||
|
|
aff7ddf41e | ||
|
|
164ae9a7a8 | ||
|
|
3aacd26b79 | ||
|
|
5915416232 | ||
|
|
c059296224 | ||
|
|
9ae70eca09 | ||
|
|
d0acf49b83 | ||
|
|
c51f3511dd | ||
|
|
ee2fcc7ee3 | ||
|
|
95615d1877 | ||
|
|
962bcda9dd | ||
|
|
9bb4739d56 | ||
|
|
de1d40f41a | ||
|
|
c0ab301160 | ||
|
|
a22df97a51 | ||
|
|
45772ade4d | ||
|
|
e8dab545f5 | ||
|
|
c2bd80207a | ||
|
|
bc5ae9a2ef | ||
|
|
36db057e32 | ||
|
|
5ac73b863a | ||
|
|
23042c33d6 | ||
|
|
4ca5f5e355 | ||
|
|
f10e5913fb | ||
|
|
8b75c11587 | ||
|
|
c287dcad3b | ||
|
|
ce6cd794c8 | ||
|
|
e05475aa5e | ||
|
|
c35e9d37ae | ||
|
|
8f2dbfe3df | ||
|
|
a0a998dfdd | ||
|
|
12491ac7c0 | ||
|
|
78e3024cec |
51
.github/workflows/main.yml
vendored
51
.github/workflows/main.yml
vendored
@@ -1,5 +1,6 @@
|
|||||||
name: build
|
name: build
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -21,38 +22,43 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
node-version: "20"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install -g pnpm
|
npm install -g pnpm
|
||||||
cd backend && pnpm i
|
cd backend && pnpm i --no-frozen-lockfile
|
||||||
- name: Test
|
# - name: Test
|
||||||
run: |
|
# run: |
|
||||||
cd backend
|
# cd backend
|
||||||
pnpm test
|
# pnpm test
|
||||||
- name: Build
|
# - name: Build
|
||||||
run: |
|
# run: |
|
||||||
cd backend
|
# cd backend
|
||||||
pnpm run build
|
# pnpm run build
|
||||||
- name: Bundle
|
- name: Bundle
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
pnpm i -D estrella
|
pnpm bundle:esbuild
|
||||||
pnpm run bundle
|
|
||||||
- id: tag
|
- id: tag
|
||||||
name: Generate release tag
|
name: Generate release tag
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||||
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
|
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
|
||||||
|
- name: Prepare release
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
pnpm i -D conventional-changelog-cli
|
||||||
|
pnpm run changelog
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
body_path: ./backend/CHANGELOG.md
|
||||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||||
generate_release_notes: true
|
# generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
./backend/sub-store.min.js
|
./backend/sub-store.min.js
|
||||||
./backend/dist/sub-store-0.min.js
|
./backend/dist/sub-store-0.min.js
|
||||||
@@ -60,8 +66,19 @@ jobs:
|
|||||||
./backend/dist/sub-store-parser.loon.min.js
|
./backend/dist/sub-store-parser.loon.min.js
|
||||||
./backend/dist/cron-sync-artifacts.min.js
|
./backend/dist/cron-sync-artifacts.min.js
|
||||||
./backend/dist/sub-store.bundle.js
|
./backend/dist/sub-store.bundle.js
|
||||||
- name: Sync to GitLab
|
- name: Git push assets to "release" branch
|
||||||
env:
|
|
||||||
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline
|
cd backend/dist || exit 1
|
||||||
|
git init
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git checkout -b release
|
||||||
|
git add .
|
||||||
|
git commit -m "release: ${{ steps.tag.outputs.release_tag }}"
|
||||||
|
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
|
||||||
|
git push -f -u origin release
|
||||||
|
# - 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
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "web"]
|
|
||||||
path = web
|
|
||||||
url = https://github.com/sub-store-org/Sub-Store-Front-End.git
|
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -1,19 +1,19 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<br>
|
<br>
|
||||||
<img width="200" src="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" alt="Sub-Store">
|
<img width="200" src="https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png" alt="Sub-Store">
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<h2 align="center">Sub-Store<h2>
|
<h2 align="center">Sub-Store<h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center" color="#6a737d">
|
<p align="center" color="#6a737d">
|
||||||
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
|
Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
[](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml)     
|
||||||
|
<a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
[](https://www.buymeacoffee.com/PengYM)
|
[](https://www.buymeacoffee.com/PengYM)
|
||||||
|
|
||||||
Core functionalities:
|
Core functionalities:
|
||||||
|
|
||||||
1. Conversion among various formats.
|
1. Conversion among various formats.
|
||||||
@@ -21,36 +21,44 @@ Core functionalities:
|
|||||||
3. Collect multiple subscriptions in one URL.
|
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.
|
> 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
|
## 1. Subscription Conversion
|
||||||
|
|
||||||
### Supported Input Formats
|
### Supported Input Formats
|
||||||
|
|
||||||
- [x] SS URI
|
> ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs.
|
||||||
- [x] SSR URI
|
|
||||||
- [x] SSD URI
|
- [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
|
||||||
- [x] V2RayN URI
|
|
||||||
- [x] Hysteria2 URI
|
example: `socks5+tls://user:pass@ip:port#name`
|
||||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
|
||||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS, Hysteria2)
|
- [x] URI(SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
|
||||||
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
|
- [x] Clash Proxies YAML
|
||||||
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, Hysteria2)
|
- [x] Clash Proxy JSON(single line)
|
||||||
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria, Hysteria2)
|
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
|
||||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
|
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
|
||||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard)
|
- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
|
||||||
|
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
|
||||||
|
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru)
|
||||||
|
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
|
||||||
|
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||||
|
|
||||||
### Supported Target Platforms
|
### Supported Target Platforms
|
||||||
|
|
||||||
- [x] QX
|
- [x] Plain JSON
|
||||||
- [x] Loon
|
|
||||||
- [x] Surge
|
|
||||||
- [x] Stash
|
- [x] Stash
|
||||||
- [x] Clash.Meta
|
- [x] Clash.Meta(mihomo)
|
||||||
- [x] Clash
|
- [x] Clash
|
||||||
- [x] ShadowRocket
|
- [x] Surfboard
|
||||||
|
- [x] Surge
|
||||||
|
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
|
||||||
|
- [x] Loon
|
||||||
|
- [x] Egern
|
||||||
|
- [x] Shadowrocket
|
||||||
|
- [x] QX
|
||||||
|
- [x] sing-box
|
||||||
- [x] V2Ray
|
- [x] V2Ray
|
||||||
- [x] V2Ray URI
|
- [x] V2Ray URI
|
||||||
- [x] Plain JSON
|
|
||||||
|
|
||||||
## 2. Subscription Formatting
|
## 2. Subscription Formatting
|
||||||
|
|
||||||
@@ -81,15 +89,25 @@ Install `pnpm`
|
|||||||
Go to `backend` directories, install node dependencies:
|
Go to `backend` directories, install node dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm install
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
1. In `backend`, run the backend server on http://localhost:3000
|
1. In `backend`, run the backend server on http://localhost:3000
|
||||||
|
|
||||||
|
babel(old school)
|
||||||
|
|
||||||
```
|
```
|
||||||
pnpm start
|
pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
esbuild(experimental)
|
||||||
|
|
||||||
|
```
|
||||||
|
SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
|
||||||
|
```
|
||||||
|
|
||||||
## LICENSE
|
## LICENSE
|
||||||
|
|
||||||
This project is under the GPL V3 LICENSE.
|
This project is under the GPL V3 LICENSE.
|
||||||
@@ -100,7 +118,6 @@ This project is under the GPL V3 LICENSE.
|
|||||||
|
|
||||||
[](https://star-history.com/#sub-store-org/sub-store&Date)
|
[](https://star-history.com/#sub-store-org/sub-store&Date)
|
||||||
|
|
||||||
|
|
||||||
## Acknowledgements
|
## 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!
|
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
||||||
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
||||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||||
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
|
* Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket!
|
||||||
* @updated: <%= updated %>
|
* @updated: <%= updated %>
|
||||||
* @version: <%= pkg.version %>
|
* @version: <%= pkg.version %>
|
||||||
* @author: Peng-YM
|
* @author: Peng-YM
|
||||||
|
|||||||
77
backend/bundle-esbuild.js
Normal file
77
backend/bundle-esbuild.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { build } = require('esbuild');
|
||||||
|
|
||||||
|
!(async () => {
|
||||||
|
const version = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
|
||||||
|
).version.trim();
|
||||||
|
|
||||||
|
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/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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for await (const artifact of artifacts) {
|
||||||
|
await build({
|
||||||
|
entryPoints: [artifact.src],
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false,
|
||||||
|
platform: 'browser',
|
||||||
|
format: 'iife',
|
||||||
|
outfile: artifact.dest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entryPoints: ['dist/sub-store.no-bundle.js'],
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: false,
|
||||||
|
platform: 'node',
|
||||||
|
format: 'cjs',
|
||||||
|
outfile: 'dist/sub-store.bundle.js',
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(__dirname, 'dist/sub-store.bundle.js'),
|
||||||
|
`// SUB_STORE_BACKEND_VERSION: ${version}
|
||||||
|
${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
|
||||||
|
encoding: 'utf8',
|
||||||
|
})}`,
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
console.log('done');
|
||||||
|
});
|
||||||
@@ -3,23 +3,49 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { build } = require('esbuild');
|
const { build } = require('esbuild');
|
||||||
|
|
||||||
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
|
!(async () => {
|
||||||
encoding: 'utf8',
|
const version = JSON.parse(
|
||||||
});
|
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
|
||||||
content = content.replace(
|
).version.trim();
|
||||||
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
|
|
||||||
'$2',
|
|
||||||
);
|
|
||||||
fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
|
|
||||||
encoding: 'utf8',
|
|
||||||
});
|
|
||||||
|
|
||||||
build({
|
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
|
||||||
entryPoints: ['dist/sub-store.no-bundle.js'],
|
encoding: 'utf8',
|
||||||
bundle: true,
|
});
|
||||||
minify: true,
|
content = content.replace(
|
||||||
sourcemap: true,
|
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
|
||||||
platform: 'node',
|
'$2',
|
||||||
format: 'cjs',
|
);
|
||||||
outfile: 'dist/sub-store.bundle.js',
|
fs.writeFileSync(
|
||||||
});
|
path.join(__dirname, 'dist/sub-store.no-bundle.js'),
|
||||||
|
content,
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entryPoints: ['dist/sub-store.no-bundle.js'],
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: true,
|
||||||
|
platform: 'node',
|
||||||
|
format: 'cjs',
|
||||||
|
outfile: 'dist/sub-store.bundle.js',
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(__dirname, 'dist/sub-store.bundle.js'),
|
||||||
|
`// SUB_STORE_BACKEND_VERSION: ${version}
|
||||||
|
${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
|
||||||
|
encoding: 'utf8',
|
||||||
|
})}`,
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
console.log('done');
|
||||||
|
});
|
||||||
|
|||||||
24
backend/dev-esbuild.js
Normal file
24
backend/dev-esbuild.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const { build } = require('esbuild');
|
||||||
|
|
||||||
|
!(async () => {
|
||||||
|
const artifacts = [{ src: 'src/main.js', dest: 'sub-store.min.js' }];
|
||||||
|
|
||||||
|
for await (const artifact of artifacts) {
|
||||||
|
await build({
|
||||||
|
entryPoints: [artifact.src],
|
||||||
|
bundle: true,
|
||||||
|
minify: false,
|
||||||
|
sourcemap: false,
|
||||||
|
platform: 'node',
|
||||||
|
format: 'cjs',
|
||||||
|
outfile: artifact.dest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
console.log('done');
|
||||||
|
});
|
||||||
@@ -1,30 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "2.14.121",
|
"version": "2.16.64",
|
||||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
||||||
"serve": "node sub-store.min.js",
|
"serve": "node sub-store.min.js",
|
||||||
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
|
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
|
||||||
|
"dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
|
||||||
|
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
|
||||||
"build": "gulp",
|
"build": "gulp",
|
||||||
"bundle": "node bundle.js"
|
"bundle": "node bundle.js",
|
||||||
|
"bundle:esbuild": "node bundle-esbuild.js",
|
||||||
|
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
|
||||||
},
|
},
|
||||||
"author": "Peng-YM",
|
"author": "Peng-YM",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@maxmind/geoip2-node": "^5.0.0",
|
||||||
"automerge": "1.0.1-preview.7",
|
"automerge": "1.0.1-preview.7",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"connect-history-api-fallback": "^2.0.0",
|
"connect-history-api-fallback": "^2.0.0",
|
||||||
|
"cron": "^3.1.6",
|
||||||
|
"dns-packet": "^5.6.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"http-proxy-middleware": "^2.0.6",
|
||||||
|
"ip-address": "^9.0.5",
|
||||||
"js-base64": "^3.7.2",
|
"js-base64": "^3.7.2",
|
||||||
|
"jsrsasign": "^11.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"nanoid": "^3.3.3",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"requests": "^0.3.0",
|
"semver": "^7.6.3",
|
||||||
"semver": "^7.3.7",
|
"static-js-yaml": "^1.0.0"
|
||||||
"static-js-yaml": "^1.0.0",
|
|
||||||
"uuid": "^8.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.0",
|
"@babel/core": "^7.18.0",
|
||||||
|
|||||||
17137
backend/pnpm-lock.yaml
generated
17137
backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,13 @@ export const FILES_KEY = 'files';
|
|||||||
export const MODULES_KEY = 'modules';
|
export const MODULES_KEY = 'modules';
|
||||||
export const ARTIFACTS_KEY = 'artifacts';
|
export const ARTIFACTS_KEY = 'artifacts';
|
||||||
export const RULES_KEY = 'rules';
|
export const RULES_KEY = 'rules';
|
||||||
|
export const TOKENS_KEY = 'tokens';
|
||||||
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
||||||
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
||||||
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
||||||
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
||||||
|
export const HEADERS_RESOURCE_CACHE_KEY = '#sub-store-cached-headers-resource';
|
||||||
|
export const CHR_EXPIRATION_TIME_KEY = '#sub-store-chr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 1 min
|
||||||
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
|
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 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
|
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import rs from '@/utils/rs';
|
||||||
|
import YAML from '@/utils/yaml';
|
||||||
import download from '@/utils/download';
|
import download from '@/utils/download';
|
||||||
import { isIPv4, isIPv6 } from '@/utils';
|
import {
|
||||||
|
isIPv4,
|
||||||
|
isIPv6,
|
||||||
|
isValidPortNumber,
|
||||||
|
isValidUUID,
|
||||||
|
isNotBlank,
|
||||||
|
ipAddress,
|
||||||
|
getRandomPort,
|
||||||
|
numberToString,
|
||||||
|
} from '@/utils';
|
||||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||||
import PROXY_PREPROCESSORS from './preprocessors';
|
import PROXY_PREPROCESSORS from './preprocessors';
|
||||||
import PROXY_PRODUCERS from './producers';
|
import PROXY_PRODUCERS from './producers';
|
||||||
import PROXY_PARSERS from './parsers';
|
import PROXY_PARSERS from './parsers';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||||
|
import { findByName } from '@/utils/database';
|
||||||
|
import { produceArtifact } from '@/restful/sync';
|
||||||
|
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
|
||||||
|
import Gist from '@/utils/gist';
|
||||||
|
import { isPresent } from './producers/utils';
|
||||||
|
import { doh } from '@/utils/dns';
|
||||||
|
|
||||||
function preprocess(raw) {
|
function preprocess(raw) {
|
||||||
for (const processor of PROXY_PREPROCESSORS) {
|
for (const processor of PROXY_PREPROCESSORS) {
|
||||||
@@ -59,12 +78,36 @@ function parse(raw) {
|
|||||||
$.error(`Failed to parse line: ${line}`);
|
$.error(`Failed to parse line: ${line}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return proxies.filter((proxy) => {
|
||||||
return proxies;
|
if (['vless', 'vmess'].includes(proxy.type)) {
|
||||||
|
const isProxyUUIDValid = isValidUUID(proxy.uuid);
|
||||||
|
if (!isProxyUUIDValid) {
|
||||||
|
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
|
||||||
|
}
|
||||||
|
// return isProxyUUIDValid;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(proxies, operators = [], targetPlatform, source) {
|
async function processFn(
|
||||||
|
proxies,
|
||||||
|
operators = [],
|
||||||
|
targetPlatform,
|
||||||
|
source,
|
||||||
|
$options,
|
||||||
|
) {
|
||||||
for (const item of operators) {
|
for (const item of operators) {
|
||||||
|
if (item.disabled) {
|
||||||
|
$.log(
|
||||||
|
`Skipping disabled operator: "${
|
||||||
|
item.type
|
||||||
|
}" with arguments:\n >>> ${
|
||||||
|
JSON.stringify(item.args, null, 2) || 'None'
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// process script
|
// process script
|
||||||
let script;
|
let script;
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
@@ -72,7 +115,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
|||||||
const { mode, content } = item.args;
|
const { mode, content } = item.args;
|
||||||
if (mode === 'link') {
|
if (mode === 'link') {
|
||||||
let noCache;
|
let noCache;
|
||||||
let url = content;
|
let url = content || '';
|
||||||
if (url.endsWith('#noCache')) {
|
if (url.endsWith('#noCache')) {
|
||||||
url = url.replace(/#noCache$/, '');
|
url = url.replace(/#noCache$/, '');
|
||||||
noCache = true;
|
noCache = true;
|
||||||
@@ -95,18 +138,50 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`;
|
||||||
|
const downloadUrlMatch = url.match(
|
||||||
|
/^\/api\/(file|module)\/(.+)/,
|
||||||
|
);
|
||||||
|
if (downloadUrlMatch) {
|
||||||
|
let type = '';
|
||||||
|
try {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
// if this is a remote script, download it
|
if (type === 'module') {
|
||||||
try {
|
script = item.content;
|
||||||
script = await download(
|
} else {
|
||||||
`${url.split('#')[0]}${noCache ? '#noCache' : ''}`,
|
script = await produceArtifact({
|
||||||
);
|
type: 'file',
|
||||||
// $.info(`Script loaded: >>>\n ${script}`);
|
name,
|
||||||
} catch (err) {
|
});
|
||||||
$.error(
|
}
|
||||||
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
} catch (err) {
|
||||||
);
|
$.error(
|
||||||
throw new Error(`无法下载脚本: ${url}`);
|
`Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`,
|
||||||
|
);
|
||||||
|
throw new Error(`无法加载 ${type}: ${url}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if this is a remote script, download it
|
||||||
|
try {
|
||||||
|
script = await download(url);
|
||||||
|
// $.info(`Script loaded: >>>\n ${script}`);
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
|
||||||
|
);
|
||||||
|
throw new Error(`无法下载脚本: ${url}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
script = content;
|
script = content;
|
||||||
@@ -118,7 +193,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.info(
|
$.log(
|
||||||
`Applying "${item.type}" with arguments:\n >>> ${
|
`Applying "${item.type}" with arguments:\n >>> ${
|
||||||
JSON.stringify(item.args, null, 2) || 'None'
|
JSON.stringify(item.args, null, 2) || 'None'
|
||||||
}`,
|
}`,
|
||||||
@@ -130,6 +205,7 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
|||||||
targetPlatform,
|
targetPlatform,
|
||||||
$arguments,
|
$arguments,
|
||||||
source,
|
source,
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
||||||
@@ -139,35 +215,73 @@ async function process(proxies, operators = [], targetPlatform, source) {
|
|||||||
return proxies;
|
return proxies;
|
||||||
}
|
}
|
||||||
|
|
||||||
function produce(proxies, targetPlatform, type) {
|
function produce(proxies, targetPlatform, type, opts = {}) {
|
||||||
const producer = PROXY_PRODUCERS[targetPlatform];
|
const producer = PROXY_PRODUCERS[targetPlatform];
|
||||||
if (!producer) {
|
if (!producer) {
|
||||||
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter unsupported proxies
|
const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
|
||||||
proxies = proxies.filter(
|
targetPlatform,
|
||||||
(proxy) =>
|
|
||||||
!(proxy.supported && proxy.supported[targetPlatform] === false),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$.info(`Producing proxies for target: ${targetPlatform}`);
|
// filter unsupported proxies
|
||||||
|
proxies = proxies.filter((proxy) => {
|
||||||
|
// 检查代理是否支持目标平台
|
||||||
|
if (proxy.supported && proxy.supported[targetPlatform] === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于 vless 和 vmess 代理,需要额外验证 UUID
|
||||||
|
if (['vless', 'vmess'].includes(proxy.type)) {
|
||||||
|
const isProxyUUIDValid = isValidUUID(proxy.uuid);
|
||||||
|
if (!isProxyUUIDValid)
|
||||||
|
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
|
||||||
|
// return isProxyUUIDValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
proxies = proxies.map((proxy) => {
|
||||||
|
proxy._resolved = proxy.resolved;
|
||||||
|
|
||||||
|
if (!isNotBlank(proxy.name)) {
|
||||||
|
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
|
||||||
|
}
|
||||||
|
if (proxy['disable-sni']) {
|
||||||
|
if (sni_off_supported) {
|
||||||
|
proxy.sni = 'off';
|
||||||
|
} else if (!['tuic'].includes(proxy.type)) {
|
||||||
|
$.error(
|
||||||
|
`Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`,
|
||||||
|
);
|
||||||
|
proxy.sni = '';
|
||||||
|
proxy['skip-cert-verify'] = true;
|
||||||
|
delete proxy['tls-fingerprint'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 端口跳跃
|
||||||
|
if (proxy.ports) {
|
||||||
|
proxy.ports = String(proxy.ports);
|
||||||
|
if (!['ClashMeta'].includes(targetPlatform)) {
|
||||||
|
proxy.ports = proxy.ports.replace(/\//g, ',');
|
||||||
|
}
|
||||||
|
if (!proxy.port) {
|
||||||
|
proxy.port = getRandomPort(proxy.ports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
});
|
||||||
|
|
||||||
|
$.log(`Producing proxies for target: ${targetPlatform}`);
|
||||||
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
|
||||||
let localPort = 10000;
|
let list = proxies
|
||||||
return proxies
|
|
||||||
.map((proxy) => {
|
.map((proxy) => {
|
||||||
try {
|
try {
|
||||||
let line = producer.produce(proxy, type);
|
return producer.produce(proxy, type, opts);
|
||||||
if (
|
|
||||||
line.length > 0 &&
|
|
||||||
line.includes('__SubStoreLocalPort__')
|
|
||||||
) {
|
|
||||||
line = line.replace(
|
|
||||||
/__SubStoreLocalPort__/g,
|
|
||||||
localPort++,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
`Cannot produce proxy: ${JSON.stringify(
|
`Cannot produce proxy: ${JSON.stringify(
|
||||||
@@ -179,20 +293,42 @@ function produce(proxies, targetPlatform, type) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((line) => line.length > 0)
|
.filter((line) => line.length > 0);
|
||||||
.join('\n');
|
list = type === 'internal' ? list : list.join('\n');
|
||||||
|
if (
|
||||||
|
targetPlatform.startsWith('Surge') &&
|
||||||
|
proxies.length > 0 &&
|
||||||
|
proxies.every((p) => p.type === 'wireguard')
|
||||||
|
) {
|
||||||
|
list = `#!name=${proxies[0]?._subName}
|
||||||
|
#!desc=${proxies[0]?._desc ?? ''}
|
||||||
|
#!category=${proxies[0]?._category ?? ''}
|
||||||
|
${list}`;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
} else if (producer.type === 'ALL') {
|
} else if (producer.type === 'ALL') {
|
||||||
return producer.produce(proxies, type);
|
return producer.produce(proxies, type, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProxyUtils = {
|
export const ProxyUtils = {
|
||||||
parse,
|
parse,
|
||||||
process,
|
process: processFn,
|
||||||
produce,
|
produce,
|
||||||
|
ipAddress,
|
||||||
|
getRandomPort,
|
||||||
isIPv4,
|
isIPv4,
|
||||||
isIPv6,
|
isIPv6,
|
||||||
isIP,
|
isIP,
|
||||||
|
yaml: YAML,
|
||||||
|
getFlag,
|
||||||
|
removeFlag,
|
||||||
|
getISO,
|
||||||
|
MMDB,
|
||||||
|
Gist,
|
||||||
|
download,
|
||||||
|
isValidUUID,
|
||||||
|
doh,
|
||||||
};
|
};
|
||||||
|
|
||||||
function tryParse(parser, line) {
|
function tryParse(parser, line) {
|
||||||
@@ -213,43 +349,118 @@ function safeMatch(parser, line) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTransportPath(path) {
|
||||||
|
if (typeof path === 'string' || typeof path === 'number') {
|
||||||
|
path = String(path).trim();
|
||||||
|
|
||||||
|
if (path === '') {
|
||||||
|
return '/';
|
||||||
|
} else if (!path.startsWith('/')) {
|
||||||
|
return '/' + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
function lastParse(proxy) {
|
function lastParse(proxy) {
|
||||||
|
if (typeof proxy.cipher === 'string') {
|
||||||
|
proxy.cipher = proxy.cipher.toLowerCase();
|
||||||
|
}
|
||||||
|
if (typeof proxy.password === 'number') {
|
||||||
|
proxy.password = numberToString(proxy.password);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['ss'].includes(proxy.type) &&
|
||||||
|
proxy.cipher === 'none' &&
|
||||||
|
!proxy.password
|
||||||
|
) {
|
||||||
|
// https://github.com/MetaCubeX/mihomo/issues/1677
|
||||||
|
proxy.password = '';
|
||||||
|
}
|
||||||
|
if (proxy.interface) {
|
||||||
|
proxy['interface-name'] = proxy.interface;
|
||||||
|
delete proxy.interface;
|
||||||
|
}
|
||||||
|
if (isValidPortNumber(proxy.port)) {
|
||||||
|
proxy.port = parseInt(proxy.port, 10);
|
||||||
|
}
|
||||||
if (proxy.server) {
|
if (proxy.server) {
|
||||||
proxy.server = proxy.server
|
proxy.server = `${proxy.server}`
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^\[/, '')
|
.replace(/^\[/, '')
|
||||||
.replace(/\]$/, '');
|
.replace(/\]$/, '');
|
||||||
}
|
}
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) {
|
||||||
|
proxy['ws-opts'] = {};
|
||||||
|
if (proxy['ws-path']) {
|
||||||
|
proxy['ws-opts'].path = proxy['ws-path'];
|
||||||
|
}
|
||||||
|
if (proxy['ws-headers']) {
|
||||||
|
proxy['ws-opts'].headers = proxy['ws-headers'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete proxy['ws-path'];
|
||||||
|
delete proxy['ws-headers'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||||
|
|
||||||
|
if (Array.isArray(transportPath)) {
|
||||||
|
proxy[`${proxy.network}-opts`].path = transportPath.map((item) =>
|
||||||
|
formatTransportPath(item),
|
||||||
|
);
|
||||||
|
} else if (transportPath != null) {
|
||||||
|
proxy[`${proxy.network}-opts`].path =
|
||||||
|
formatTransportPath(transportPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (proxy.type === 'trojan') {
|
if (proxy.type === 'trojan') {
|
||||||
if (proxy.network === 'tcp') {
|
if (proxy.network === 'tcp') {
|
||||||
delete proxy.network;
|
delete proxy.network;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
|
if (['vless'].includes(proxy.type)) {
|
||||||
|
if (!proxy.network) {
|
||||||
|
proxy.network = 'tcp';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'trojan',
|
||||||
|
'tuic',
|
||||||
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
|
'juicity',
|
||||||
|
'anytls',
|
||||||
|
].includes(proxy.type)
|
||||||
|
) {
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
}
|
}
|
||||||
if (proxy.network) {
|
if (proxy.network) {
|
||||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||||
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
|
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
|
||||||
if (transporthost && !transportHost) {
|
if (proxy.network === 'h2') {
|
||||||
|
if (!transporthost && transportHost) {
|
||||||
|
proxy[`${proxy.network}-opts`].headers.host = transportHost;
|
||||||
|
delete proxy[`${proxy.network}-opts`].headers.Host;
|
||||||
|
}
|
||||||
|
} else if (transporthost && !transportHost) {
|
||||||
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
|
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
|
||||||
delete proxy[`${proxy.network}-opts`].headers.host;
|
delete proxy[`${proxy.network}-opts`].headers.host;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (proxy.tls && !proxy.sni) {
|
if (proxy.network === 'h2') {
|
||||||
if (proxy.network) {
|
const host = proxy['h2-opts']?.headers?.host;
|
||||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
const path = proxy['h2-opts']?.path;
|
||||||
transportHost = Array.isArray(transportHost)
|
if (host && !Array.isArray(host)) {
|
||||||
? transportHost[0]
|
proxy['h2-opts'].headers.host = [host];
|
||||||
: transportHost;
|
|
||||||
if (transportHost) {
|
|
||||||
proxy.sni = transportHost;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!proxy.sni && !isIP(proxy.server)) {
|
if (Array.isArray(path)) {
|
||||||
proxy.sni = proxy.server;
|
proxy['h2-opts'].path = path[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
|
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
|
||||||
if (
|
if (
|
||||||
!proxy.tls &&
|
!proxy.tls &&
|
||||||
@@ -276,6 +487,136 @@ function lastParse(proxy) {
|
|||||||
proxy[`${proxy.network}-opts`].path = [transportPath];
|
proxy[`${proxy.network}-opts`].path = [transportPath];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (proxy.tls && !proxy.sni) {
|
||||||
|
if (!isIP(proxy.server)) {
|
||||||
|
proxy.sni = proxy.server;
|
||||||
|
}
|
||||||
|
if (!proxy.sni && proxy.network) {
|
||||||
|
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||||
|
transportHost = Array.isArray(transportHost)
|
||||||
|
? transportHost[0]
|
||||||
|
: transportHost;
|
||||||
|
if (transportHost) {
|
||||||
|
proxy.sni = transportHost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) {
|
||||||
|
if (proxy.ports) {
|
||||||
|
proxy.ports = String(proxy.ports).replace(/\//g, ',');
|
||||||
|
} else {
|
||||||
|
delete proxy.ports;
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
if (
|
||||||
|
['hysteria2'].includes(proxy.type) &&
|
||||||
|
proxy.obfs &&
|
||||||
|
!['salamander'].includes(proxy.obfs) &&
|
||||||
|
!proxy['obfs-password']
|
||||||
|
) {
|
||||||
|
proxy['obfs-password'] = proxy.obfs;
|
||||||
|
proxy.obfs = 'salamander';
|
||||||
|
}
|
||||||
|
if (['vless'].includes(proxy.type)) {
|
||||||
|
// 删除 reality-opts: {}
|
||||||
|
if (
|
||||||
|
proxy['reality-opts'] &&
|
||||||
|
Object.keys(proxy['reality-opts']).length === 0
|
||||||
|
) {
|
||||||
|
delete proxy['reality-opts'];
|
||||||
|
}
|
||||||
|
// 删除 grpc-opts: {}
|
||||||
|
if (
|
||||||
|
proxy['grpc-opts'] &&
|
||||||
|
Object.keys(proxy['grpc-opts']).length === 0
|
||||||
|
) {
|
||||||
|
delete proxy['grpc-opts'];
|
||||||
|
}
|
||||||
|
// 非 reality, 空 flow 没有意义
|
||||||
|
if (!proxy['reality-opts'] && !proxy.flow) {
|
||||||
|
delete proxy.flow;
|
||||||
|
}
|
||||||
|
if (['http'].includes(proxy.network)) {
|
||||||
|
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||||
|
if (!transportPath) {
|
||||||
|
if (!proxy[`${proxy.network}-opts`]) {
|
||||||
|
proxy[`${proxy.network}-opts`] = {};
|
||||||
|
}
|
||||||
|
proxy[`${proxy.network}-opts`].path = ['/'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof proxy.name !== 'string') {
|
||||||
|
if (/^\d+$/.test(proxy.name)) {
|
||||||
|
proxy.name = `${proxy.name}`;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (proxy.name?.data) {
|
||||||
|
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
|
||||||
|
} else {
|
||||||
|
proxy.name = Buffer.from(proxy.name).toString('utf8');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`proxy.name decode failed\nReason: ${e}`);
|
||||||
|
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (['ws', 'http', 'h2'].includes(proxy.network)) {
|
||||||
|
if (
|
||||||
|
['ws', 'h2'].includes(proxy.network) &&
|
||||||
|
!proxy[`${proxy.network}-opts`]?.path
|
||||||
|
) {
|
||||||
|
proxy[`${proxy.network}-opts`] =
|
||||||
|
proxy[`${proxy.network}-opts`] || {};
|
||||||
|
proxy[`${proxy.network}-opts`].path = '/';
|
||||||
|
} else if (
|
||||||
|
proxy.network === 'http' &&
|
||||||
|
(!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||
|
||||||
|
proxy[`${proxy.network}-opts`]?.path.every((i) => !i))
|
||||||
|
) {
|
||||||
|
proxy[`${proxy.network}-opts`] =
|
||||||
|
proxy[`${proxy.network}-opts`] || {};
|
||||||
|
proxy[`${proxy.network}-opts`].path = ['/'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (['', 'off'].includes(proxy.sni)) {
|
||||||
|
proxy['disable-sni'] = true;
|
||||||
|
}
|
||||||
|
let caStr = proxy['ca_str'];
|
||||||
|
if (proxy['ca-str']) {
|
||||||
|
caStr = proxy['ca-str'];
|
||||||
|
} else if (caStr) {
|
||||||
|
delete proxy['ca_str'];
|
||||||
|
proxy['ca-str'] = caStr;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if ($.env.isNode && !caStr && proxy['_ca']) {
|
||||||
|
caStr = $.node.fs.readFileSync(proxy['_ca'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`Read ca file failed\nReason: ${e}`);
|
||||||
|
}
|
||||||
|
if (!proxy['tls-fingerprint'] && caStr) {
|
||||||
|
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['shadowsocks'].includes(proxy.type) &&
|
||||||
|
isPresent(proxy, 'shadow-tls-password')
|
||||||
|
) {
|
||||||
|
proxy.plugin = 'shadow-tls';
|
||||||
|
proxy['plugin-opts'] = {
|
||||||
|
host: proxy['shadow-tls-sni'],
|
||||||
|
password: proxy['shadow-tls-password'],
|
||||||
|
version: proxy['shadow-tls-version'],
|
||||||
|
};
|
||||||
|
delete proxy['shadow-tls-sni'];
|
||||||
|
delete proxy['shadow-tls-password'];
|
||||||
|
delete proxy['shadow-tls-version'];
|
||||||
|
}
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,16 +35,16 @@ const grammars = String.raw`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
|
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{
|
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)*{
|
||||||
proxy.type = "ssr";
|
proxy.type = "ssr";
|
||||||
// handle ssr obfs
|
// handle ssr obfs
|
||||||
proxy.obfs = obfs.type;
|
proxy.obfs = obfs.type;
|
||||||
}
|
}
|
||||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (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/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)* {
|
||||||
proxy.type = "ss";
|
proxy.type = "ss";
|
||||||
// handle ss obfs
|
// handle ss obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -54,30 +54,33 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
|
|||||||
$set(proxy, "plugin-opts.path", obfs.path);
|
$set(proxy, "plugin-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)* {
|
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "vmess";
|
proxy.type = "vmess";
|
||||||
proxy.cipher = proxy.cipher || "none";
|
proxy.cipher = proxy.cipher || "none";
|
||||||
proxy.alterId = proxy.alterId || 0;
|
proxy.alterId = proxy.alterId || 0;
|
||||||
handleTransport();
|
handleTransport();
|
||||||
}
|
}
|
||||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "vless";
|
proxy.type = "vless";
|
||||||
handleTransport();
|
handleTransport();
|
||||||
}
|
}
|
||||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "trojan";
|
proxy.type = "trojan";
|
||||||
handleTransport();
|
handleTransport();
|
||||||
}
|
}
|
||||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
|
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/others)* {
|
||||||
proxy.type = "hysteria2";
|
proxy.type = "hysteria2";
|
||||||
}
|
}
|
||||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
}
|
}
|
||||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
|
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
}
|
}
|
||||||
|
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
|
proxy.type = "socks5";
|
||||||
|
}
|
||||||
|
|
||||||
address = comma server:server comma port:port {
|
address = comma server:server comma port:port {
|
||||||
proxy.server = server;
|
proxy.server = server;
|
||||||
@@ -117,7 +120,7 @@ port = digits:[0-9]+ {
|
|||||||
method = comma cipher:cipher {
|
method = comma cipher:cipher {
|
||||||
proxy.cipher = cipher;
|
proxy.cipher = cipher;
|
||||||
}
|
}
|
||||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
|
||||||
|
|
||||||
username = & {
|
username = & {
|
||||||
let j = peg$currPos;
|
let j = peg$currPos;
|
||||||
@@ -166,15 +169,24 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
|
|||||||
|
|
||||||
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
|
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
|
||||||
|
|
||||||
|
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
|
||||||
|
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(""); }
|
||||||
|
|
||||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||||
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; }
|
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
|
||||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||||
|
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
|
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
|
|
||||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||||
|
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||||
|
|
||||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||||
|
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
|
||||||
|
|
||||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||||
comma = _ "," _
|
comma = _ "," _
|
||||||
|
|||||||
@@ -33,16 +33,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
|
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{
|
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)*{
|
||||||
proxy.type = "ssr";
|
proxy.type = "ssr";
|
||||||
// handle ssr obfs
|
// handle ssr obfs
|
||||||
proxy.obfs = obfs.type;
|
proxy.obfs = obfs.type;
|
||||||
}
|
}
|
||||||
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (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/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/others)* {
|
||||||
proxy.type = "ss";
|
proxy.type = "ss";
|
||||||
// handle ss obfs
|
// handle ss obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -52,30 +52,33 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
|
|||||||
$set(proxy, "plugin-opts.path", obfs.path);
|
$set(proxy, "plugin-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)* {
|
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "vmess";
|
proxy.type = "vmess";
|
||||||
proxy.cipher = proxy.cipher || "none";
|
proxy.cipher = proxy.cipher || "none";
|
||||||
proxy.alterId = proxy.alterId || 0;
|
proxy.alterId = proxy.alterId || 0;
|
||||||
handleTransport();
|
handleTransport();
|
||||||
}
|
}
|
||||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "vless";
|
proxy.type = "vless";
|
||||||
handleTransport();
|
handleTransport();
|
||||||
}
|
}
|
||||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
|
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "trojan";
|
proxy.type = "trojan";
|
||||||
handleTransport();
|
handleTransport();
|
||||||
}
|
}
|
||||||
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
|
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/others)* {
|
||||||
proxy.type = "hysteria2";
|
proxy.type = "hysteria2";
|
||||||
}
|
}
|
||||||
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
|
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
}
|
}
|
||||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
|
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
}
|
}
|
||||||
|
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/others)* {
|
||||||
|
proxy.type = "socks5";
|
||||||
|
}
|
||||||
|
|
||||||
address = comma server:server comma port:port {
|
address = comma server:server comma port:port {
|
||||||
proxy.server = server;
|
proxy.server = server;
|
||||||
@@ -115,7 +118,7 @@ port = digits:[0-9]+ {
|
|||||||
method = comma cipher:cipher {
|
method = comma cipher:cipher {
|
||||||
proxy.cipher = cipher;
|
proxy.cipher = cipher;
|
||||||
}
|
}
|
||||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
|
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
|
||||||
|
|
||||||
username = & {
|
username = & {
|
||||||
let j = peg$currPos;
|
let j = peg$currPos;
|
||||||
@@ -164,15 +167,24 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
|
|||||||
|
|
||||||
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
|
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
|
||||||
|
|
||||||
|
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
|
||||||
|
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(""); }
|
||||||
|
|
||||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||||
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; }
|
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
|
||||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||||
|
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
|
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
|
|
||||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
||||||
|
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||||
|
|
||||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||||
|
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
|
||||||
|
|
||||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||||
comma = _ "," _
|
comma = _ "," _
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const grammars = String.raw`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (trojan/shadowsocks/vmess/http/socks5) {
|
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +50,11 @@ trojan = "trojan" equals address
|
|||||||
|
|
||||||
shadowsocks = "shadowsocks" equals address
|
shadowsocks = "shadowsocks" equals address
|
||||||
(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)* {
|
(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) {
|
if (proxy.protocol || proxy.type === "ssr") {
|
||||||
proxy.type = "ssr";
|
proxy.type = "ssr";
|
||||||
|
if (!proxy.protocol) {
|
||||||
|
proxy.protocol = "origin";
|
||||||
|
}
|
||||||
// handle ssr obfs
|
// handle ssr obfs
|
||||||
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
||||||
if (obfs.type) proxy.obfs = obfs.type;
|
if (obfs.type) proxy.obfs = obfs.type;
|
||||||
@@ -91,6 +94,13 @@ vmess = "vmess" equals address
|
|||||||
handleObfs();
|
handleObfs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vless = "vless" equals address
|
||||||
|
(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 = "vless";
|
||||||
|
proxy.cipher = proxy.cipher || "none";
|
||||||
|
handleObfs();
|
||||||
|
}
|
||||||
|
|
||||||
http = "http" equals address
|
http = "http" equals address
|
||||||
(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)*{
|
(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";
|
proxy.type = "http";
|
||||||
@@ -142,7 +152,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
|
|||||||
method = comma "method" equals cipher:cipher {
|
method = comma "method" equals cipher:cipher {
|
||||||
proxy.cipher = cipher;
|
proxy.cipher = cipher;
|
||||||
};
|
};
|
||||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305");
|
||||||
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
||||||
|
|
||||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||||
@@ -165,7 +175,7 @@ tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
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; }
|
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; }
|
||||||
obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
|
obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
|
||||||
|
|
||||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (trojan/shadowsocks/vmess/http/socks5) {
|
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +48,11 @@ trojan = "trojan" equals address
|
|||||||
|
|
||||||
shadowsocks = "shadowsocks" equals address
|
shadowsocks = "shadowsocks" equals address
|
||||||
(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)* {
|
(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) {
|
if (proxy.protocol || proxy.type === "ssr") {
|
||||||
proxy.type = "ssr";
|
proxy.type = "ssr";
|
||||||
|
if (!proxy.protocol) {
|
||||||
|
proxy.protocol = "origin";
|
||||||
|
}
|
||||||
// handle ssr obfs
|
// handle ssr obfs
|
||||||
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
||||||
if (obfs.type) proxy.obfs = obfs.type;
|
if (obfs.type) proxy.obfs = obfs.type;
|
||||||
@@ -89,6 +92,13 @@ vmess = "vmess" equals address
|
|||||||
handleObfs();
|
handleObfs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vless = "vless" equals address
|
||||||
|
(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 = "vless";
|
||||||
|
proxy.cipher = proxy.cipher || "none";
|
||||||
|
handleObfs();
|
||||||
|
}
|
||||||
|
|
||||||
http = "http" equals address
|
http = "http" equals address
|
||||||
(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)*{
|
(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";
|
proxy.type = "http";
|
||||||
@@ -140,7 +150,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
|
|||||||
method = comma "method" equals cipher:cipher {
|
method = comma "method" equals cipher:cipher {
|
||||||
proxy.cipher = cipher;
|
proxy.cipher = cipher;
|
||||||
};
|
};
|
||||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305");
|
||||||
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
||||||
|
|
||||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||||
@@ -163,7 +173,7 @@ tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
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; }
|
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; obfs.type = type; return type; }
|
||||||
obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
|
obfs = comma "obfs" equals type:("wss"/"ws"/"over-tls"/"http") { obfs.type = type; return type; };
|
||||||
|
|
||||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
||||||
|
|||||||
@@ -30,13 +30,18 @@ const grammars = String.raw`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function handleShadowTLS() {
|
||||||
|
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
|
||||||
|
proxy['shadow-tls-version'] = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/direct) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)* {
|
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
|
||||||
proxy.type = "ss";
|
proxy.type = "ss";
|
||||||
// handle obfs
|
// handle obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -45,8 +50,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
|||||||
$set(proxy, "plugin-opts.host", obfs.host);
|
$set(proxy, "plugin-opts.host", obfs.host);
|
||||||
$set(proxy, "plugin-opts.path", obfs.path);
|
$set(proxy, "plugin-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "vmess";
|
proxy.type = "vmess";
|
||||||
proxy.cipher = proxy.cipher || "none";
|
proxy.cipher = proxy.cipher || "none";
|
||||||
if (proxy.aead) {
|
if (proxy.aead) {
|
||||||
@@ -55,19 +61,27 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
|||||||
proxy.alterId = proxy.alterId || 0;
|
proxy.alterId = proxy.alterId || 0;
|
||||||
}
|
}
|
||||||
handleWebsocket();
|
handleWebsocket();
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "trojan";
|
proxy.type = "trojan";
|
||||||
handleWebsocket();
|
handleWebsocket();
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
|
proxy.type = "ssh";
|
||||||
|
handleShadowTLS();
|
||||||
|
}
|
||||||
|
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "snell";
|
proxy.type = "snell";
|
||||||
// handle obfs
|
// handle obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -75,26 +89,36 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
|||||||
$set(proxy, "obfs-opts.host", obfs.host);
|
$set(proxy, "obfs-opts.host", obfs.host);
|
||||||
$set(proxy, "obfs-opts.path", obfs.path);
|
$set(proxy, "obfs-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
proxy.version = 5;
|
proxy.version = 5;
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "wireguard-surge";
|
proxy.type = "wireguard-surge";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/fast_open/tfo/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||||
proxy.type = "hysteria2";
|
proxy.type = "hysteria2";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "socks5";
|
proxy.type = "socks5";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "socks5";
|
proxy.type = "socks5";
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
|
handleShadowTLS();
|
||||||
|
}
|
||||||
|
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
|
||||||
|
proxy.type = "direct";
|
||||||
}
|
}
|
||||||
|
|
||||||
address = comma server:server comma port:port {
|
address = comma server:server comma port:port {
|
||||||
@@ -130,6 +154,8 @@ port = digits:[0-9]+ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
|
||||||
|
|
||||||
username = & {
|
username = & {
|
||||||
let j = peg$currPos;
|
let j = peg$currPos;
|
||||||
let start, end;
|
let start, end;
|
||||||
@@ -156,7 +182,13 @@ username = & {
|
|||||||
password = comma match:[^,]+ { proxy.password = match.join(""); }
|
password = comma match:[^,]+ { proxy.password = match.join(""); }
|
||||||
|
|
||||||
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
||||||
sni = comma "sni" equals sni:domain { proxy.sni = sni; }
|
sni = comma "sni" equals sni:("off"/domain) {
|
||||||
|
if (sni === "off") {
|
||||||
|
proxy["disable-sni"] = true;
|
||||||
|
} else {
|
||||||
|
proxy.sni = sni;
|
||||||
|
}
|
||||||
|
}
|
||||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||||
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||||
|
|
||||||
@@ -164,14 +196,14 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
|||||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||||
|
|
||||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
||||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||||
|
|
||||||
method = comma "encrypt-method" equals cipher:cipher {
|
method = comma "encrypt-method" equals cipher:cipher {
|
||||||
proxy.cipher = cipher;
|
proxy.cipher = cipher;
|
||||||
}
|
}
|
||||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
|
||||||
|
|
||||||
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
|
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
|
||||||
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
|
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
|
||||||
@@ -190,7 +222,7 @@ obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
|
|||||||
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
||||||
uri = $[^,]+
|
uri = $[^,]+
|
||||||
|
|
||||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||||
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||||
@@ -201,7 +233,17 @@ no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-ale
|
|||||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = 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(""); }
|
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||||
|
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
|
||||||
|
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
|
||||||
|
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
|
||||||
|
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
|
||||||
|
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
|
||||||
|
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
|
||||||
|
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
|
||||||
|
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
|
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||||
|
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
|
||||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
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_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(""); }
|
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
|
||||||
|
|||||||
@@ -28,13 +28,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function handleShadowTLS() {
|
||||||
|
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
|
||||||
|
proxy['shadow-tls-version'] = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
|
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/direct) {
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)* {
|
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
|
||||||
proxy.type = "ss";
|
proxy.type = "ss";
|
||||||
// handle obfs
|
// handle obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -43,8 +48,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
|||||||
$set(proxy, "plugin-opts.host", obfs.host);
|
$set(proxy, "plugin-opts.host", obfs.host);
|
||||||
$set(proxy, "plugin-opts.path", obfs.path);
|
$set(proxy, "plugin-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "vmess";
|
proxy.type = "vmess";
|
||||||
proxy.cipher = proxy.cipher || "none";
|
proxy.cipher = proxy.cipher || "none";
|
||||||
if (proxy.aead) {
|
if (proxy.aead) {
|
||||||
@@ -53,19 +59,27 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
|
|||||||
proxy.alterId = proxy.alterId || 0;
|
proxy.alterId = proxy.alterId || 0;
|
||||||
}
|
}
|
||||||
handleWebsocket();
|
handleWebsocket();
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "trojan";
|
proxy.type = "trojan";
|
||||||
handleWebsocket();
|
handleWebsocket();
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "http";
|
proxy.type = "http";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
|
proxy.type = "ssh";
|
||||||
|
handleShadowTLS();
|
||||||
|
}
|
||||||
|
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "snell";
|
proxy.type = "snell";
|
||||||
// handle obfs
|
// handle obfs
|
||||||
if (obfs.type == "http" || obfs.type === "tls") {
|
if (obfs.type == "http" || obfs.type === "tls") {
|
||||||
@@ -73,28 +87,37 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
|
|||||||
$set(proxy, "obfs-opts.host", obfs.host);
|
$set(proxy, "obfs-opts.host", obfs.host);
|
||||||
$set(proxy, "obfs-opts.path", obfs.path);
|
$set(proxy, "obfs-opts.path", obfs.path);
|
||||||
}
|
}
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||||
proxy.type = "tuic";
|
proxy.type = "tuic";
|
||||||
proxy.version = 5;
|
proxy.version = 5;
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "wireguard-surge";
|
proxy.type = "wireguard-surge";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
|
||||||
proxy.type = "hysteria2";
|
proxy.type = "hysteria2";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "socks5";
|
proxy.type = "socks5";
|
||||||
|
handleShadowTLS();
|
||||||
}
|
}
|
||||||
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)* {
|
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "socks5";
|
proxy.type = "socks5";
|
||||||
proxy.tls = true;
|
proxy.tls = true;
|
||||||
|
handleShadowTLS();
|
||||||
|
}
|
||||||
|
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
|
||||||
|
proxy.type = "direct";
|
||||||
}
|
}
|
||||||
|
|
||||||
address = comma server:server comma port:port {
|
address = comma server:server comma port:port {
|
||||||
proxy.server = server;
|
proxy.server = server;
|
||||||
proxy.port = port;
|
proxy.port = port;
|
||||||
@@ -128,6 +151,8 @@ port = digits:[0-9]+ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
|
||||||
|
|
||||||
username = & {
|
username = & {
|
||||||
let j = peg$currPos;
|
let j = peg$currPos;
|
||||||
let start, end;
|
let start, end;
|
||||||
@@ -154,7 +179,13 @@ username = & {
|
|||||||
password = comma match:[^,]+ { proxy.password = match.join(""); }
|
password = comma match:[^,]+ { proxy.password = match.join(""); }
|
||||||
|
|
||||||
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
||||||
sni = comma "sni" equals sni:domain { proxy.sni = sni; }
|
sni = comma "sni" equals sni:("off"/domain) {
|
||||||
|
if (sni === "off") {
|
||||||
|
proxy["disable-sni"] = true;
|
||||||
|
} else {
|
||||||
|
proxy.sni = sni;
|
||||||
|
}
|
||||||
|
}
|
||||||
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
|
||||||
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||||
|
|
||||||
@@ -162,14 +193,14 @@ snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
|||||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||||
|
|
||||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
|
||||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
|
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||||
|
|
||||||
method = comma "encrypt-method" equals cipher:cipher {
|
method = comma "encrypt-method" equals cipher:cipher {
|
||||||
proxy.cipher = cipher;
|
proxy.cipher = cipher;
|
||||||
}
|
}
|
||||||
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
|
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
|
||||||
|
|
||||||
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
|
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
|
||||||
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
|
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
|
||||||
@@ -188,7 +219,7 @@ obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
|
|||||||
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
||||||
uri = $[^,]+
|
uri = $[^,]+
|
||||||
|
|
||||||
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
|
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||||
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||||
@@ -199,7 +230,17 @@ no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-ale
|
|||||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = 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(""); }
|
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||||
|
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
|
||||||
|
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
|
||||||
|
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
|
||||||
|
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
|
||||||
|
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
|
||||||
|
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
|
||||||
|
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
|
||||||
|
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
|
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||||
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
|
||||||
|
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
|
||||||
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
|
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_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(""); }
|
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ start = (trojan) {
|
|||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
|
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
|
||||||
proxy.type = "trojan";
|
proxy.type = "trojan";
|
||||||
proxy.password = password;
|
proxy.password = password;
|
||||||
proxy.server = server;
|
proxy.server = server;
|
||||||
@@ -79,9 +79,14 @@ port = digits:[0-9]+ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params = "/"? "?" head:param tail:("&"@param)* {
|
params = "?" head:param tail:("&"@param)* {
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
params[key] = decodeURIComponent(value);
|
||||||
|
}
|
||||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||||
proxy.sni = params["sni"] || params["peer"];
|
proxy.sni = params["sni"] || params["peer"];
|
||||||
|
proxy['client-fingerprint'] = params.fp;
|
||||||
|
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
|
||||||
|
|
||||||
if (toBool(params["ws"])) {
|
if (toBool(params["ws"])) {
|
||||||
proxy.network = "ws";
|
proxy.network = "ws";
|
||||||
@@ -89,12 +94,50 @@ params = "/"? "?" head:param tail:("&"@param)* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params["type"]) {
|
if (params["type"]) {
|
||||||
|
let httpupgrade
|
||||||
proxy.network = params["type"]
|
proxy.network = params["type"]
|
||||||
if (params["path"]) {
|
if(proxy.network === 'httpupgrade') {
|
||||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
proxy.network = 'ws'
|
||||||
|
httpupgrade = true
|
||||||
}
|
}
|
||||||
if (params["host"]) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
proxy[proxy.network + '-opts'] = {
|
||||||
|
'grpc-service-name': params["serviceName"],
|
||||||
|
'_grpc-type': params["mode"],
|
||||||
|
'_grpc-authority': params["authority"],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
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"]));
|
||||||
|
}
|
||||||
|
if (httpupgrade) {
|
||||||
|
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
|
||||||
|
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (['reality'].includes(params.security)) {
|
||||||
|
const opts = {};
|
||||||
|
if (params.pbk) {
|
||||||
|
opts['public-key'] = params.pbk;
|
||||||
|
}
|
||||||
|
if (params.sid) {
|
||||||
|
opts['short-id'] = params.sid;
|
||||||
|
}
|
||||||
|
if (params.spx) {
|
||||||
|
opts['_spider-x'] = params.spx;
|
||||||
|
}
|
||||||
|
if (params.mode) {
|
||||||
|
proxy._mode = params.mode;
|
||||||
|
}
|
||||||
|
if (params.extra) {
|
||||||
|
proxy._extra = params.extra;
|
||||||
|
}
|
||||||
|
if (Object.keys(opts).length > 0) {
|
||||||
|
$set(proxy, params.security+"-opts", opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ start = (trojan) {
|
|||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
|
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
|
||||||
proxy.type = "trojan";
|
proxy.type = "trojan";
|
||||||
proxy.password = password;
|
proxy.password = password;
|
||||||
proxy.server = server;
|
proxy.server = server;
|
||||||
@@ -77,9 +77,14 @@ port = digits:[0-9]+ {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
params = "/"? "?" head:param tail:("&"@param)* {
|
params = "?" head:param tail:("&"@param)* {
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
params[key] = decodeURIComponent(value);
|
||||||
|
}
|
||||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||||
proxy.sni = params["sni"] || params["peer"];
|
proxy.sni = params["sni"] || params["peer"];
|
||||||
|
proxy['client-fingerprint'] = params.fp;
|
||||||
|
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
|
||||||
|
|
||||||
if (toBool(params["ws"])) {
|
if (toBool(params["ws"])) {
|
||||||
proxy.network = "ws";
|
proxy.network = "ws";
|
||||||
@@ -87,12 +92,50 @@ params = "/"? "?" head:param tail:("&"@param)* {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params["type"]) {
|
if (params["type"]) {
|
||||||
|
let httpupgrade
|
||||||
proxy.network = params["type"]
|
proxy.network = params["type"]
|
||||||
if (params["path"]) {
|
if(proxy.network === 'httpupgrade') {
|
||||||
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
|
proxy.network = 'ws'
|
||||||
|
httpupgrade = true
|
||||||
}
|
}
|
||||||
if (params["host"]) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
|
proxy[proxy.network + '-opts'] = {
|
||||||
|
'grpc-service-name': params["serviceName"],
|
||||||
|
'_grpc-type': params["mode"],
|
||||||
|
'_grpc-authority': params["authority"],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
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"]));
|
||||||
|
}
|
||||||
|
if (httpupgrade) {
|
||||||
|
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
|
||||||
|
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (['reality'].includes(params.security)) {
|
||||||
|
const opts = {};
|
||||||
|
if (params.pbk) {
|
||||||
|
opts['public-key'] = params.pbk;
|
||||||
|
}
|
||||||
|
if (params.sid) {
|
||||||
|
opts['short-id'] = params.sid;
|
||||||
|
}
|
||||||
|
if (params.spx) {
|
||||||
|
opts['_spider-x'] = params.spx;
|
||||||
|
}
|
||||||
|
if (params.mode) {
|
||||||
|
proxy._mode = params.mode;
|
||||||
|
}
|
||||||
|
if (params.extra) {
|
||||||
|
proxy._extra = params.extra;
|
||||||
|
}
|
||||||
|
if (Object.keys(opts).length > 0) {
|
||||||
|
$set(proxy, params.security+"-opts", opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { safeLoad } from 'static-js-yaml';
|
import { safeLoad } from '@/utils/yaml';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
function HTML() {
|
function HTML() {
|
||||||
const name = 'HTML';
|
const name = 'HTML';
|
||||||
@@ -15,6 +16,7 @@ function Base64Encoded() {
|
|||||||
const keys = [
|
const keys = [
|
||||||
'dm1lc3M', // vmess
|
'dm1lc3M', // vmess
|
||||||
'c3NyOi8v', // ssr://
|
'c3NyOi8v', // ssr://
|
||||||
|
'c29ja3M6Ly', // socks://
|
||||||
'dHJvamFu', // trojan
|
'dHJvamFu', // trojan
|
||||||
'c3M6Ly', // ss:/
|
'c3M6Ly', // ss:/
|
||||||
'c3NkOi8v', // ssd://
|
'c3NkOi8v', // ssd://
|
||||||
@@ -22,6 +24,10 @@ function Base64Encoded() {
|
|||||||
'aHR0c', // htt
|
'aHR0c', // htt
|
||||||
'dmxlc3M=', // vless
|
'dmxlc3M=', // vless
|
||||||
'aHlzdGVyaWEy', // hysteria2
|
'aHlzdGVyaWEy', // hysteria2
|
||||||
|
'aHkyOi8v', // hy2://
|
||||||
|
'd2lyZWd1YXJkOi8v', // wireguard://
|
||||||
|
'd2c6Ly8=', // wg://
|
||||||
|
'dHVpYzovLw==', // tuic://
|
||||||
];
|
];
|
||||||
|
|
||||||
const test = function (raw) {
|
const test = function (raw) {
|
||||||
@@ -31,8 +37,15 @@ function Base64Encoded() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
const parse = function (raw) {
|
const parse = function (raw) {
|
||||||
raw = Base64.decode(raw);
|
const decoded = Base64.decode(raw);
|
||||||
return raw;
|
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
|
||||||
|
$.error(
|
||||||
|
`Base64 Pre-processor error: decoded line does not start with protocol`,
|
||||||
|
);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
};
|
};
|
||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
@@ -44,10 +57,48 @@ function Clash() {
|
|||||||
const content = safeLoad(raw);
|
const content = safeLoad(raw);
|
||||||
return content.proxies && Array.isArray(content.proxies);
|
return content.proxies && Array.isArray(content.proxies);
|
||||||
};
|
};
|
||||||
const parse = function (raw) {
|
const parse = function (raw, includeProxies) {
|
||||||
// Clash YAML format
|
// Clash YAML format
|
||||||
const proxies = safeLoad(raw).proxies;
|
|
||||||
return proxies.map((p) => JSON.stringify(p)).join('\n');
|
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
|
||||||
|
// 匹配 short-id 冒号后面的值(包含空格和引号)
|
||||||
|
const afterReplace = raw.replace(
|
||||||
|
/short-id:([ \t]*[^#\n,}]*)/g,
|
||||||
|
(matched, value) => {
|
||||||
|
const afterTrim = value.trim();
|
||||||
|
|
||||||
|
// 为空
|
||||||
|
if (!afterTrim || afterTrim === '') {
|
||||||
|
return 'short-id: ""';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否被引号包裹
|
||||||
|
if (/^(['"]).*\1$/.test(afterTrim)) {
|
||||||
|
return `short-id: ${afterTrim}`;
|
||||||
|
} else {
|
||||||
|
return `short-id: "${afterTrim}"`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
proxies,
|
||||||
|
'global-client-fingerprint': globalClientFingerprint,
|
||||||
|
} = safeLoad(afterReplace);
|
||||||
|
return (
|
||||||
|
(includeProxies ? 'proxies:\n' : '') +
|
||||||
|
proxies
|
||||||
|
.map((p) => {
|
||||||
|
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
|
||||||
|
if (globalClientFingerprint && !p['client-fingerprint']) {
|
||||||
|
p['client-fingerprint'] = globalClientFingerprint;
|
||||||
|
}
|
||||||
|
return `${includeProxies ? ' - ' : ''}${JSON.stringify(
|
||||||
|
p,
|
||||||
|
)}\n`;
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,68 @@
|
|||||||
import resourceCache from '@/utils/resource-cache';
|
import resourceCache from '@/utils/resource-cache';
|
||||||
import scriptResourceCache from '@/utils/script-resource-cache';
|
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||||
import { isIPv4, isIPv6 } from '@/utils';
|
import { isIPv4, isIPv6, ipAddress } from '@/utils';
|
||||||
import { FULL } from '@/utils/logical';
|
import { FULL } from '@/utils/logical';
|
||||||
import { getFlag } from '@/utils/geo';
|
import { getFlag, removeFlag } from '@/utils/geo';
|
||||||
|
import { doh } from '@/utils/dns';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
import { ProxyUtils } from '@/core/proxy-utils';
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
import env from '@/utils/env';
|
import { produceArtifact } from '@/restful/sync';
|
||||||
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
|
import { SETTINGS_KEY } from '@/constants';
|
||||||
|
import YAML from '@/utils/yaml';
|
||||||
|
|
||||||
|
import env from '@/utils/env';
|
||||||
|
import {
|
||||||
|
getFlowField,
|
||||||
|
getFlowHeaders,
|
||||||
|
parseFlowHeaders,
|
||||||
|
validCheck,
|
||||||
|
flowTransfer,
|
||||||
|
getRmainingDays,
|
||||||
|
normalizeFlowHeader,
|
||||||
|
} from '@/utils/flow';
|
||||||
|
|
||||||
|
function isObject(item) {
|
||||||
|
return item && typeof item === 'object' && !Array.isArray(item);
|
||||||
|
}
|
||||||
|
function trimWrap(str) {
|
||||||
|
if (str.startsWith('<') && str.endsWith('>')) {
|
||||||
|
return str.slice(1, -1);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
function deepMerge(target, _other) {
|
||||||
|
const other = typeof _other === 'string' ? JSON.parse(_other) : _other;
|
||||||
|
for (const key in other) {
|
||||||
|
if (isObject(other[key])) {
|
||||||
|
if (key.endsWith('!')) {
|
||||||
|
const k = trimWrap(key.slice(0, -1));
|
||||||
|
target[k] = other[key];
|
||||||
|
} else {
|
||||||
|
const k = trimWrap(key);
|
||||||
|
if (!target[k]) Object.assign(target, { [k]: {} });
|
||||||
|
deepMerge(target[k], other[k]);
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(other[key])) {
|
||||||
|
if (key.startsWith('+')) {
|
||||||
|
const k = trimWrap(key.slice(1));
|
||||||
|
if (!target[k]) Object.assign(target, { [k]: [] });
|
||||||
|
target[k] = [...other[key], ...target[k]];
|
||||||
|
} else if (key.endsWith('+')) {
|
||||||
|
const k = trimWrap(key.slice(0, -1));
|
||||||
|
if (!target[k]) Object.assign(target, { [k]: [] });
|
||||||
|
target[k] = [...target[k], ...other[key]];
|
||||||
|
} else {
|
||||||
|
const k = trimWrap(key);
|
||||||
|
Object.assign(target, { [k]: other[key] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.assign(target, { [key]: other[key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
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:
|
||||||
{
|
{
|
||||||
@@ -79,12 +132,15 @@ function QuickSettingOperator(args) {
|
|||||||
if (get(args.useless)) {
|
if (get(args.useless)) {
|
||||||
const filter = UselessFilter();
|
const filter = UselessFilter();
|
||||||
const selected = filter.func(proxies);
|
const selected = filter.func(proxies);
|
||||||
proxies.filter((_, i) => selected[i]);
|
proxies = proxies.filter(
|
||||||
|
(p, i) => selected[i] && p.port > 0 && p.port <= 65535,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxies.map((proxy) => {
|
return proxies.map((proxy) => {
|
||||||
proxy.udp = get(args.udp, proxy.udp);
|
proxy.udp = get(args.udp, proxy.udp);
|
||||||
proxy.tfo = get(args.tfo, proxy.tfo);
|
proxy.tfo = get(args.tfo, proxy.tfo);
|
||||||
|
proxy['fast-open'] = get(args.tfo, proxy['fast-open']);
|
||||||
proxy['skip-cert-verify'] = get(
|
proxy['skip-cert-verify'] = get(
|
||||||
args.scert,
|
args.scert,
|
||||||
proxy['skip-cert-verify'],
|
proxy['skip-cert-verify'],
|
||||||
@@ -110,7 +166,7 @@ function QuickSettingOperator(args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add or remove flag for proxies
|
// add or remove flag for proxies
|
||||||
function FlagOperator({ mode }) {
|
function FlagOperator({ mode, tw }) {
|
||||||
return {
|
return {
|
||||||
name: 'Flag Operator',
|
name: 'Flag Operator',
|
||||||
func: (proxies) => {
|
func: (proxies) => {
|
||||||
@@ -124,7 +180,13 @@ function FlagOperator({ mode }) {
|
|||||||
// remove old flag
|
// remove old flag
|
||||||
proxy.name = removeFlag(proxy.name);
|
proxy.name = removeFlag(proxy.name);
|
||||||
proxy.name = newFlag + ' ' + proxy.name;
|
proxy.name = newFlag + ' ' + proxy.name;
|
||||||
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
|
if (tw == 'ws') {
|
||||||
|
proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸');
|
||||||
|
} else if (tw == 'tw') {
|
||||||
|
// 不变
|
||||||
|
} else {
|
||||||
|
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return proxy;
|
return proxy;
|
||||||
});
|
});
|
||||||
@@ -296,16 +358,57 @@ function RegexDeleteOperator(regex) {
|
|||||||
1. This function name should be `operator`!
|
1. This function name should be `operator`!
|
||||||
2. Always declare variables before using them!
|
2. Always declare variables before using them!
|
||||||
*/
|
*/
|
||||||
function ScriptOperator(script, targetPlatform, $arguments, source) {
|
function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
|
||||||
return {
|
return {
|
||||||
name: 'Script Operator',
|
name: 'Script Operator',
|
||||||
func: async (proxies) => {
|
func: async (proxies) => {
|
||||||
let output = proxies;
|
let output = proxies;
|
||||||
|
if (output?.$file?.type === 'mihomoProfile') {
|
||||||
|
try {
|
||||||
|
let patch = YAML.safeLoad(script);
|
||||||
|
let config;
|
||||||
|
if (output?.$content) {
|
||||||
|
try {
|
||||||
|
config = YAML.safeLoad(output?.$content);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(e.message ?? e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if (typeof patch !== 'object') patch = {};
|
||||||
|
if (typeof patch !== 'object')
|
||||||
|
throw new Error('patch is not an object');
|
||||||
|
output.$content = ProxyUtils.yaml.safeDump(
|
||||||
|
deepMerge(
|
||||||
|
config ||
|
||||||
|
(output?.$file?.sourceType === 'none'
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
proxies: await produceArtifact({
|
||||||
|
type:
|
||||||
|
output?.$file?.sourceType ||
|
||||||
|
'collection',
|
||||||
|
name: output?.$file?.sourceName,
|
||||||
|
platform: 'mihomo',
|
||||||
|
produceType: 'internal',
|
||||||
|
produceOpts: {
|
||||||
|
'delete-underscore-fields': true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
patch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return output;
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
await (async function () {
|
await (async function () {
|
||||||
const operator = createDynamicFunction(
|
const operator = createDynamicFunction(
|
||||||
'operator',
|
'operator',
|
||||||
script,
|
script,
|
||||||
$arguments,
|
$arguments,
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
output = operator(proxies, targetPlatform, { source, ...env });
|
output = operator(proxies, targetPlatform, { source, ...env });
|
||||||
})();
|
})();
|
||||||
@@ -316,13 +419,48 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
|||||||
await (async function () {
|
await (async function () {
|
||||||
const operator = createDynamicFunction(
|
const operator = createDynamicFunction(
|
||||||
'operator',
|
'operator',
|
||||||
`async function operator(proxies = []) {
|
`async function operator(input = []) {
|
||||||
return proxies.map(($server = {}) => {
|
if (input && (input.$files || input.$content)) {
|
||||||
${script}
|
let { $content, $files, $options, $file } = input
|
||||||
return $server
|
if($file.type === 'mihomoProfile') {
|
||||||
})
|
${script}
|
||||||
|
if(typeof main === 'function') {
|
||||||
|
let config;
|
||||||
|
if ($content) {
|
||||||
|
try {
|
||||||
|
config = ProxyUtils.yaml.safeLoad($content);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message ?? e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {
|
||||||
|
proxies: await produceArtifact({
|
||||||
|
type: $file.sourceType || 'collection',
|
||||||
|
name: $file.sourceName,
|
||||||
|
platform: 'mihomo',
|
||||||
|
produceType: 'internal',
|
||||||
|
produceOpts: {
|
||||||
|
'delete-underscore-fields': true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
${script}
|
||||||
|
}
|
||||||
|
return { $content, $files, $options, $file }
|
||||||
|
} else {
|
||||||
|
let proxies = input
|
||||||
|
let list = []
|
||||||
|
for await (let $server of proxies) {
|
||||||
|
${script}
|
||||||
|
list.push($server)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
}`,
|
}`,
|
||||||
$arguments,
|
$arguments,
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
output = operator(proxies, targetPlatform, { source, ...env });
|
output = operator(proxies, targetPlatform, { source, ...env });
|
||||||
})();
|
})();
|
||||||
@@ -331,128 +469,231 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOMAIN_RESOLVERS = {
|
function parseIP4P(IP4P) {
|
||||||
Google: async function (domain) {
|
let server;
|
||||||
const id = hex_md5(`GOOGLE:${domain}`);
|
let port;
|
||||||
const cached = resourceCache.get(id);
|
try {
|
||||||
if (cached) return cached;
|
let array = IP4P.split(':');
|
||||||
const resp = await $.http.get({
|
|
||||||
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
|
port = parseInt(array[2], 16);
|
||||||
domain,
|
let ipab = parseInt(array[3], 16);
|
||||||
)}&type=A`,
|
let ipcd = parseInt(array[4], 16);
|
||||||
headers: {
|
let ipa = ipab >> 8;
|
||||||
accept: 'application/dns-json',
|
let ipb = ipab & 0xff;
|
||||||
},
|
let ipc = ipcd >> 8;
|
||||||
});
|
let ipd = ipcd & 0xff;
|
||||||
const body = JSON.parse(resp.body);
|
server = `${ipa}.${ipb}.${ipc}.${ipd}`;
|
||||||
if (body['Status'] !== 0) {
|
if (port <= 0 || port > 65535) {
|
||||||
throw new Error(`Status is ${body['Status']}`);
|
throw new Error(`Invalid port number: ${port}`);
|
||||||
}
|
}
|
||||||
const answers = body['Answer'];
|
if (!isIPv4(server)) {
|
||||||
if (answers.length === 0) {
|
throw new Error(`Invalid IP address: ${server}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// throw new Error(`IP4P 解析失败: ${e}`);
|
||||||
|
$.error(`IP4P 解析失败: ${e}`);
|
||||||
|
}
|
||||||
|
return { server, port };
|
||||||
|
}
|
||||||
|
|
||||||
|
const DOMAIN_RESOLVERS = {
|
||||||
|
Custom: async function (domain, type, noCache, timeout, edns, url) {
|
||||||
|
const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
|
||||||
|
const cached = resourceCache.get(id);
|
||||||
|
if (!noCache && cached) return cached;
|
||||||
|
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
|
||||||
|
const res = await doh({
|
||||||
|
url,
|
||||||
|
domain,
|
||||||
|
type: answerType,
|
||||||
|
timeout,
|
||||||
|
edns,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { answers } = res;
|
||||||
|
if (!Array.isArray(answers) || answers.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers
|
||||||
|
.filter((i) => i?.type === answerType)
|
||||||
|
.map((i) => i?.data)
|
||||||
|
.filter((i) => i);
|
||||||
|
if (result.length === 0) {
|
||||||
throw new Error('No answers');
|
throw new Error('No answers');
|
||||||
}
|
}
|
||||||
const result = answers[answers.length - 1].data;
|
|
||||||
resourceCache.set(id, result);
|
resourceCache.set(id, result);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
'IP-API': async function (domain) {
|
Google: async function (domain, type, noCache, timeout, edns) {
|
||||||
|
const id = hex_md5(`GOOGLE:${domain}:${type}`);
|
||||||
|
const cached = resourceCache.get(id);
|
||||||
|
if (!noCache && cached) return cached;
|
||||||
|
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
|
||||||
|
const res = await doh({
|
||||||
|
url: 'https://8.8.4.4/dns-query',
|
||||||
|
domain,
|
||||||
|
type: answerType,
|
||||||
|
timeout,
|
||||||
|
edns,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { answers } = res;
|
||||||
|
if (!Array.isArray(answers) || answers.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers
|
||||||
|
.filter((i) => i?.type === answerType)
|
||||||
|
.map((i) => i?.data)
|
||||||
|
.filter((i) => i);
|
||||||
|
if (result.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
resourceCache.set(id, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
'IP-API': async function (domain, type, noCache, timeout) {
|
||||||
|
if (['IPv6'].includes(type)) {
|
||||||
|
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
|
||||||
|
}
|
||||||
const id = hex_md5(`IP-API:${domain}`);
|
const id = hex_md5(`IP-API:${domain}`);
|
||||||
const cached = resourceCache.get(id);
|
const cached = resourceCache.get(id);
|
||||||
if (cached) return cached;
|
if (!noCache && cached) return cached;
|
||||||
const resp = await $.http.get({
|
const resp = await $.http.get({
|
||||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||||
domain,
|
domain,
|
||||||
)}?lang=zh-CN`,
|
)}?lang=zh-CN`,
|
||||||
|
timeout,
|
||||||
});
|
});
|
||||||
const body = JSON.parse(resp.body);
|
const body = JSON.parse(resp.body);
|
||||||
if (body['status'] !== 'success') {
|
if (body['status'] !== 'success') {
|
||||||
throw new Error(`Status is ${body['status']}`);
|
throw new Error(`Status is ${body['status']}`);
|
||||||
}
|
}
|
||||||
const result = body.query;
|
if (!body.query || body.query === 0) {
|
||||||
resourceCache.set(id, result);
|
throw new Error('No answers');
|
||||||
return result;
|
}
|
||||||
},
|
const result = [body.query];
|
||||||
Cloudflare: async function (domain) {
|
if (result.length === 0) {
|
||||||
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,
|
|
||||||
)}&type=A`,
|
|
||||||
headers: {
|
|
||||||
accept: 'application/dns-json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const body = JSON.parse(resp.body);
|
|
||||||
if (body['Status'] !== 0) {
|
|
||||||
throw new Error(`Status is ${body['Status']}`);
|
|
||||||
}
|
|
||||||
const answers = body['Answer'];
|
|
||||||
if (answers.length === 0) {
|
|
||||||
throw new Error('No answers');
|
throw new Error('No answers');
|
||||||
}
|
}
|
||||||
const result = answers[answers.length - 1].data;
|
|
||||||
resourceCache.set(id, result);
|
resourceCache.set(id, result);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
Ali: async function (domain) {
|
Cloudflare: async function (domain, type, noCache, timeout, edns) {
|
||||||
const id = hex_md5(`ALI:${domain}`);
|
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
|
||||||
const cached = resourceCache.get(id);
|
const cached = resourceCache.get(id);
|
||||||
if (cached) return cached;
|
if (!noCache && cached) return cached;
|
||||||
|
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
|
||||||
|
const res = await doh({
|
||||||
|
url: 'https://1.0.0.1/dns-query',
|
||||||
|
domain,
|
||||||
|
type: answerType,
|
||||||
|
timeout,
|
||||||
|
edns,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { answers } = res;
|
||||||
|
if (!Array.isArray(answers) || answers.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers
|
||||||
|
.filter((i) => i?.type === answerType)
|
||||||
|
.map((i) => i?.data)
|
||||||
|
.filter((i) => i);
|
||||||
|
if (result.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
resourceCache.set(id, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
Ali: async function (domain, type, noCache, timeout, edns) {
|
||||||
|
const id = hex_md5(`ALI:${domain}:${type}`);
|
||||||
|
const cached = resourceCache.get(id);
|
||||||
|
if (!noCache && cached) return cached;
|
||||||
const resp = await $.http.get({
|
const resp = await $.http.get({
|
||||||
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
|
url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/24&name=${encodeURIComponent(
|
||||||
domain,
|
domain,
|
||||||
)}&type=A&short=1`,
|
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
|
||||||
headers: {
|
headers: {
|
||||||
accept: 'application/dns-json',
|
accept: 'application/dns-json',
|
||||||
},
|
},
|
||||||
|
timeout,
|
||||||
});
|
});
|
||||||
const answers = JSON.parse(resp.body);
|
const answers = JSON.parse(resp.body);
|
||||||
if (answers.length === 0) {
|
if (!Array.isArray(answers) || answers.length === 0) {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers;
|
||||||
|
if (result.length === 0) {
|
||||||
throw new Error('No answers');
|
throw new Error('No answers');
|
||||||
}
|
}
|
||||||
const result = answers[answers.length - 1];
|
|
||||||
resourceCache.set(id, result);
|
resourceCache.set(id, result);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
Tencent: async function (domain) {
|
Tencent: async function (domain, type, noCache, timeout, edns) {
|
||||||
const id = hex_md5(`ALI:${domain}`);
|
const id = hex_md5(`TENCENT:${domain}:${type}`);
|
||||||
const cached = resourceCache.get(id);
|
const cached = resourceCache.get(id);
|
||||||
if (cached) return cached;
|
if (!noCache && cached) return cached;
|
||||||
const resp = await $.http.get({
|
const resp = await $.http.get({
|
||||||
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
|
url: `http://119.28.28.28/d?ip=${edns}&type=${
|
||||||
domain,
|
type === 'IPv6' ? 'AAAA' : 'A'
|
||||||
)}`,
|
}&dn=${encodeURIComponent(domain)}`,
|
||||||
headers: {
|
headers: {
|
||||||
accept: 'application/dns-json',
|
accept: 'application/dns-json',
|
||||||
},
|
},
|
||||||
|
timeout,
|
||||||
});
|
});
|
||||||
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
|
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
|
||||||
if (answers.length === 0) {
|
if (answers.length === 0 || String(answers) === '0') {
|
||||||
|
throw new Error('No answers');
|
||||||
|
}
|
||||||
|
const result = answers;
|
||||||
|
if (result.length === 0) {
|
||||||
throw new Error('No answers');
|
throw new Error('No answers');
|
||||||
}
|
}
|
||||||
const result = answers[answers.length - 1];
|
|
||||||
resourceCache.set(id, result);
|
resourceCache.set(id, result);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function ResolveDomainOperator({ provider }) {
|
function ResolveDomainOperator({
|
||||||
|
provider,
|
||||||
|
type: _type,
|
||||||
|
filter,
|
||||||
|
cache,
|
||||||
|
url,
|
||||||
|
timeout,
|
||||||
|
edns: _edns,
|
||||||
|
}) {
|
||||||
|
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
|
||||||
|
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
|
||||||
|
}
|
||||||
|
const { defaultTimeout } = $.read(SETTINGS_KEY);
|
||||||
|
const requestTimeout = timeout || defaultTimeout || 8000;
|
||||||
|
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
|
||||||
|
|
||||||
const resolver = DOMAIN_RESOLVERS[provider];
|
const resolver = DOMAIN_RESOLVERS[provider];
|
||||||
if (!resolver) {
|
if (!resolver) {
|
||||||
throw new Error(`Cannot find resolver: ${provider}`);
|
throw new Error(`找不到域名解析服务提供方: ${provider}`);
|
||||||
}
|
}
|
||||||
|
let edns = _edns || '223.6.6.6';
|
||||||
|
if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
|
||||||
|
$.info(
|
||||||
|
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
name: 'Resolve Domain Operator',
|
name: 'Resolve Domain Operator',
|
||||||
func: async (proxies) => {
|
func: async (proxies) => {
|
||||||
|
proxies.forEach((p, i) => {
|
||||||
|
if (!p['_no-resolve'] && p['no-resolve']) {
|
||||||
|
proxies[i]['_no-resolve'] = p['no-resolve'];
|
||||||
|
}
|
||||||
|
});
|
||||||
const results = {};
|
const results = {};
|
||||||
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
|
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
|
||||||
const totalDomain = [
|
const totalDomain = [
|
||||||
...new Set(
|
...new Set(
|
||||||
proxies
|
proxies
|
||||||
.filter((p) => !isIP(p.server) && !p['no-resolve'])
|
.filter((p) => !isIP(p.server) && !p['_no-resolve'])
|
||||||
.map((c) => c.server),
|
.map((c) => c.server),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -461,7 +702,14 @@ function ResolveDomainOperator({ provider }) {
|
|||||||
const currentBatch = [];
|
const currentBatch = [];
|
||||||
for (let domain of totalDomain.splice(0, limit)) {
|
for (let domain of totalDomain.splice(0, limit)) {
|
||||||
currentBatch.push(
|
currentBatch.push(
|
||||||
resolver(domain)
|
resolver(
|
||||||
|
domain,
|
||||||
|
type,
|
||||||
|
cache === 'disabled',
|
||||||
|
requestTimeout,
|
||||||
|
edns,
|
||||||
|
url,
|
||||||
|
)
|
||||||
.then((ip) => {
|
.then((ip) => {
|
||||||
results[domain] = ip;
|
results[domain] = ip;
|
||||||
$.info(
|
$.info(
|
||||||
@@ -478,17 +726,76 @@ function ResolveDomainOperator({ provider }) {
|
|||||||
await Promise.all(currentBatch);
|
await Promise.all(currentBatch);
|
||||||
}
|
}
|
||||||
proxies.forEach((p) => {
|
proxies.forEach((p) => {
|
||||||
if (!p['no-resolve']) {
|
if (!p['_no-resolve']) {
|
||||||
if (results[p.server]) {
|
if (results[p.server]) {
|
||||||
p.server = results[p.server];
|
p._resolved_ips = results[p.server];
|
||||||
p.resolved = true;
|
let ip = Array.isArray(results[p.server])
|
||||||
} else {
|
? results[p.server][
|
||||||
|
Math.floor(
|
||||||
|
Math.random() * results[p.server].length,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: results[p.server];
|
||||||
|
if (type === 'IPv6' && isIPv6(ip)) {
|
||||||
|
try {
|
||||||
|
ip = new ipAddress.Address6(ip).correctForm();
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`Failed to parse IPv6 address: ${ip}: ${e}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) {
|
||||||
|
p._IP4P = ip;
|
||||||
|
const { server, port } = parseIP4P(ip);
|
||||||
|
if (server && port) {
|
||||||
|
p._domain = p.server;
|
||||||
|
p.server = server;
|
||||||
|
p.port = port;
|
||||||
|
p.resolved = true;
|
||||||
|
p._IPv4 = p.server;
|
||||||
|
if (!isIP(p._IP)) {
|
||||||
|
p._IP = p.server;
|
||||||
|
}
|
||||||
|
} else if (!p.resolved) {
|
||||||
|
p.resolved = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p._domain = p.server;
|
||||||
|
p.server = ip;
|
||||||
|
p.resolved = true;
|
||||||
|
p[`_${type}`] = p.server;
|
||||||
|
if (!isIP(p._IP)) {
|
||||||
|
p._IP = p.server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p._domain = p.server;
|
||||||
|
p.server = ip;
|
||||||
|
p.resolved = true;
|
||||||
|
p[`_${type}`] = p.server;
|
||||||
|
if (!isIP(p._IP)) {
|
||||||
|
p._IP = p.server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!p.resolved) {
|
||||||
p.resolved = false;
|
p.resolved = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return proxies;
|
return proxies.filter((p) => {
|
||||||
|
if (filter === 'removeFailed') {
|
||||||
|
return isIP(p.server) || p['_no-resolve'] || p.resolved;
|
||||||
|
} else if (filter === 'IPOnly') {
|
||||||
|
return isIP(p.server);
|
||||||
|
} else if (filter === 'IPv4Only') {
|
||||||
|
return isIPv4(p.server);
|
||||||
|
} else if (filter === 'IPv6Only') {
|
||||||
|
return isIPv6(p.server);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -499,29 +806,55 @@ function isIP(ip) {
|
|||||||
|
|
||||||
ResolveDomainOperator.resolver = DOMAIN_RESOLVERS;
|
ResolveDomainOperator.resolver = DOMAIN_RESOLVERS;
|
||||||
|
|
||||||
|
function isAscii(str) {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
var pattern = /^[\x00-\x7F]+$/; // ASCII 范围的 Unicode 编码
|
||||||
|
return pattern.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
/**************************** Filters ***************************************/
|
/**************************** Filters ***************************************/
|
||||||
// filter useless proxies
|
// filter useless proxies
|
||||||
function UselessFilter() {
|
function UselessFilter() {
|
||||||
const KEYWORDS = [
|
|
||||||
'网址',
|
|
||||||
'流量',
|
|
||||||
'时间',
|
|
||||||
'应急',
|
|
||||||
'过期',
|
|
||||||
'Bandwidth',
|
|
||||||
'expire',
|
|
||||||
];
|
|
||||||
return {
|
return {
|
||||||
name: 'Useless Filter',
|
name: 'Useless Filter',
|
||||||
func: RegexFilter({
|
func: (proxies) => {
|
||||||
regex: KEYWORDS,
|
return proxies.map((proxy) => {
|
||||||
keep: false,
|
if (proxy.cipher && !isAscii(proxy.cipher)) {
|
||||||
}).func,
|
return false;
|
||||||
|
} else if (proxy.password && !isAscii(proxy.password)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (proxy.network) {
|
||||||
|
let transportHosts =
|
||||||
|
proxy[`${proxy.network}-opts`]?.headers?.Host ||
|
||||||
|
proxy[`${proxy.network}-opts`]?.headers?.host;
|
||||||
|
transportHosts = Array.isArray(transportHosts)
|
||||||
|
? transportHosts
|
||||||
|
: [transportHosts];
|
||||||
|
if (
|
||||||
|
transportHosts.some(
|
||||||
|
(host) => host && !isAscii(host),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !/网址|流量|时间|应急|过期|Bandwidth|expire/.test(
|
||||||
|
proxy.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter by regions
|
// filter by regions
|
||||||
function RegionFilter(regions) {
|
function RegionFilter(input) {
|
||||||
|
let regions = input?.value || input;
|
||||||
|
if (!Array.isArray(regions)) {
|
||||||
|
regions = [];
|
||||||
|
}
|
||||||
|
const keep = input?.keep || true;
|
||||||
const REGION_MAP = {
|
const REGION_MAP = {
|
||||||
HK: '🇭🇰',
|
HK: '🇭🇰',
|
||||||
TW: '🇹🇼',
|
TW: '🇹🇼',
|
||||||
@@ -529,6 +862,8 @@ function RegionFilter(regions) {
|
|||||||
SG: '🇸🇬',
|
SG: '🇸🇬',
|
||||||
JP: '🇯🇵',
|
JP: '🇯🇵',
|
||||||
UK: '🇬🇧',
|
UK: '🇬🇧',
|
||||||
|
DE: '🇩🇪',
|
||||||
|
KR: '🇰🇷',
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
name: 'Region Filter',
|
name: 'Region Filter',
|
||||||
@@ -536,7 +871,8 @@ function RegionFilter(regions) {
|
|||||||
// this would be high memory usage
|
// this would be high memory usage
|
||||||
return proxies.map((proxy) => {
|
return proxies.map((proxy) => {
|
||||||
const flag = getFlag(proxy.name);
|
const flag = getFlag(proxy.name);
|
||||||
return regions.some((r) => REGION_MAP[r] === flag);
|
const selected = regions.some((r) => REGION_MAP[r] === flag);
|
||||||
|
return keep ? selected : !selected;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -568,11 +904,19 @@ function buildRegex(str, ...options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filter by proxy types
|
// filter by proxy types
|
||||||
function TypeFilter(types) {
|
function TypeFilter(input) {
|
||||||
|
let types = input?.value || input;
|
||||||
|
if (!Array.isArray(types)) {
|
||||||
|
types = [];
|
||||||
|
}
|
||||||
|
const keep = input?.keep || true;
|
||||||
return {
|
return {
|
||||||
name: 'Type Filter',
|
name: 'Type Filter',
|
||||||
func: (proxies) => {
|
func: (proxies) => {
|
||||||
return proxies.map((proxy) => types.some((t) => proxy.type === t));
|
return proxies.map((proxy) => {
|
||||||
|
const selected = types.some((t) => proxy.type === t);
|
||||||
|
return keep ? selected : !selected;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -590,7 +934,7 @@ function TypeFilter(types) {
|
|||||||
1. This function name should be `filter`!
|
1. This function name should be `filter`!
|
||||||
2. Always declare variables before using them!
|
2. Always declare variables before using them!
|
||||||
*/
|
*/
|
||||||
function ScriptFilter(script, targetPlatform, $arguments, source) {
|
function ScriptFilter(script, targetPlatform, $arguments, source, $options) {
|
||||||
return {
|
return {
|
||||||
name: 'Script Filter',
|
name: 'Script Filter',
|
||||||
func: async (proxies) => {
|
func: async (proxies) => {
|
||||||
@@ -600,6 +944,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
|
|||||||
'filter',
|
'filter',
|
||||||
script,
|
script,
|
||||||
$arguments,
|
$arguments,
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
output = filter(proxies, targetPlatform, { source, ...env });
|
output = filter(proxies, targetPlatform, { source, ...env });
|
||||||
})();
|
})();
|
||||||
@@ -610,12 +955,19 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
|
|||||||
await (async function () {
|
await (async function () {
|
||||||
const filter = createDynamicFunction(
|
const filter = createDynamicFunction(
|
||||||
'filter',
|
'filter',
|
||||||
`async function filter(proxies = []) {
|
`async function filter(input = []) {
|
||||||
return proxies.filter(($server = {}) => {
|
let proxies = input
|
||||||
${script}
|
let list = []
|
||||||
})
|
const fn = async ($server) => {
|
||||||
|
${script}
|
||||||
|
}
|
||||||
|
for await (let $server of proxies) {
|
||||||
|
list.push(await fn($server))
|
||||||
|
}
|
||||||
|
return list
|
||||||
}`,
|
}`,
|
||||||
$arguments,
|
$arguments,
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
output = filter(proxies, targetPlatform, { source, ...env });
|
output = filter(proxies, targetPlatform, { source, ...env });
|
||||||
})();
|
})();
|
||||||
@@ -649,20 +1001,21 @@ async function ApplyFilter(filter, objs) {
|
|||||||
try {
|
try {
|
||||||
selected = await filter.func(objs);
|
selected = await filter.func(objs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// print log and skip this filter
|
|
||||||
$.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
|
|
||||||
let funcErr = '';
|
let funcErr = '';
|
||||||
let funcErrMsg = `${err.message ?? err}`;
|
let funcErrMsg = `${err.message ?? err}`;
|
||||||
if (funcErrMsg.includes('$server is not defined')) {
|
if (funcErrMsg.includes('$server is not defined')) {
|
||||||
funcErr = '';
|
funcErr = '';
|
||||||
} else {
|
} else {
|
||||||
|
$.error(
|
||||||
|
`Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
|
||||||
|
);
|
||||||
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
|
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
selected = await filter.nodeFunc(objs);
|
selected = await filter.nodeFunc(objs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
`Cannot apply filter ${filter.name}(node script)! Reason: ${err}`,
|
`Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`,
|
||||||
);
|
);
|
||||||
let nodeErr = '';
|
let nodeErr = '';
|
||||||
let nodeErrMsg = `${err.message ?? err}`;
|
let nodeErrMsg = `${err.message ?? err}`;
|
||||||
@@ -670,7 +1023,7 @@ async function ApplyFilter(filter, objs) {
|
|||||||
nodeErr = '';
|
nodeErr = '';
|
||||||
funcErr = `执行失败 ${funcErrMsg}`;
|
funcErr = `执行失败 ${funcErrMsg}`;
|
||||||
} else {
|
} else {
|
||||||
nodeErr = `执行节点快捷过滤脚本 失败 ${nodeErr}`;
|
nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;
|
||||||
}
|
}
|
||||||
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
|
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
|
||||||
}
|
}
|
||||||
@@ -684,14 +1037,20 @@ async function ApplyOperator(operator, objs) {
|
|||||||
const output_ = await operator.func(output);
|
const output_ = await operator.func(output);
|
||||||
if (output_) output = output_;
|
if (output_) output = output_;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
|
||||||
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
|
||||||
);
|
|
||||||
let funcErr = '';
|
let funcErr = '';
|
||||||
let funcErrMsg = `${err.message ?? err}`;
|
let funcErrMsg = `${err.message ?? err}`;
|
||||||
if (funcErrMsg.includes('$server is not defined')) {
|
if (
|
||||||
|
funcErrMsg.includes('$server is not defined') ||
|
||||||
|
funcErrMsg.includes('$content is not defined') ||
|
||||||
|
funcErrMsg.includes('$files is not defined') ||
|
||||||
|
output?.$files ||
|
||||||
|
output?.$content
|
||||||
|
) {
|
||||||
funcErr = '';
|
funcErr = '';
|
||||||
} else {
|
} else {
|
||||||
|
$.error(
|
||||||
|
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
|
||||||
|
);
|
||||||
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
|
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -699,7 +1058,7 @@ async function ApplyOperator(operator, objs) {
|
|||||||
if (output_) output = output_;
|
if (output_) output = output_;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
`Cannot apply operator ${operator.name}(node script)! Reason: ${err}`,
|
`Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`,
|
||||||
);
|
);
|
||||||
let nodeErr = '';
|
let nodeErr = '';
|
||||||
let nodeErrMsg = `${err.message ?? err}`;
|
let nodeErrMsg = `${err.message ?? err}`;
|
||||||
@@ -707,7 +1066,7 @@ async function ApplyOperator(operator, objs) {
|
|||||||
nodeErr = '';
|
nodeErr = '';
|
||||||
funcErr = `执行失败 ${funcErrMsg}`;
|
funcErr = `执行失败 ${funcErrMsg}`;
|
||||||
} else {
|
} else {
|
||||||
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
|
nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;
|
||||||
}
|
}
|
||||||
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
|
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
|
||||||
}
|
}
|
||||||
@@ -749,18 +1108,20 @@ function clone(object) {
|
|||||||
return JSON.parse(JSON.stringify(object));
|
return JSON.parse(JSON.stringify(object));
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove flag
|
function createDynamicFunction(name, script, $arguments, $options) {
|
||||||
function removeFlag(str) {
|
const flowUtils = {
|
||||||
return str
|
getFlowField,
|
||||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
|
getFlowHeaders,
|
||||||
.trim();
|
parseFlowHeaders,
|
||||||
}
|
flowTransfer,
|
||||||
|
validCheck,
|
||||||
function createDynamicFunction(name, script, $arguments) {
|
getRmainingDays,
|
||||||
const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer };
|
normalizeFlowHeader,
|
||||||
|
};
|
||||||
if ($.env.isLoon) {
|
if ($.env.isLoon) {
|
||||||
return new Function(
|
return new Function(
|
||||||
'$arguments',
|
'$arguments',
|
||||||
|
'$options',
|
||||||
'$substore',
|
'$substore',
|
||||||
'lodash',
|
'lodash',
|
||||||
'$persistentStore',
|
'$persistentStore',
|
||||||
@@ -769,9 +1130,12 @@ function createDynamicFunction(name, script, $arguments) {
|
|||||||
'ProxyUtils',
|
'ProxyUtils',
|
||||||
'scriptResourceCache',
|
'scriptResourceCache',
|
||||||
'flowUtils',
|
'flowUtils',
|
||||||
|
'produceArtifact',
|
||||||
|
'require',
|
||||||
`${script}\n return ${name}`,
|
`${script}\n return ${name}`,
|
||||||
)(
|
)(
|
||||||
$arguments,
|
$arguments,
|
||||||
|
$options,
|
||||||
$,
|
$,
|
||||||
lodash,
|
lodash,
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@@ -783,17 +1147,31 @@ function createDynamicFunction(name, script, $arguments) {
|
|||||||
ProxyUtils,
|
ProxyUtils,
|
||||||
scriptResourceCache,
|
scriptResourceCache,
|
||||||
flowUtils,
|
flowUtils,
|
||||||
|
produceArtifact,
|
||||||
|
eval(`typeof require !== "undefined"`) ? require : undefined,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return new Function(
|
return new Function(
|
||||||
'$arguments',
|
'$arguments',
|
||||||
|
'$options',
|
||||||
'$substore',
|
'$substore',
|
||||||
'lodash',
|
'lodash',
|
||||||
'ProxyUtils',
|
'ProxyUtils',
|
||||||
'scriptResourceCache',
|
'scriptResourceCache',
|
||||||
'flowUtils',
|
'flowUtils',
|
||||||
|
'produceArtifact',
|
||||||
|
'require',
|
||||||
`${script}\n return ${name}`,
|
`${script}\n return ${name}`,
|
||||||
)($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
|
)(
|
||||||
|
$arguments,
|
||||||
|
$options,
|
||||||
|
$,
|
||||||
|
lodash,
|
||||||
|
ProxyUtils,
|
||||||
|
scriptResourceCache,
|
||||||
|
flowUtils,
|
||||||
|
produceArtifact,
|
||||||
|
eval(`typeof require !== "undefined"`) ? require : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,118 +2,192 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
|
|
||||||
export default function Clash_Producer() {
|
export default function Clash_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
// VLESS XTLS is not supported by Clash
|
// VLESS XTLS is not supported by Clash
|
||||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
||||||
// github.com/Dreamacro/clash/pull/2891/files
|
// github.com/Dreamacro/clash/pull/2891/files
|
||||||
// filter unsupported proxies
|
// filter unsupported proxies
|
||||||
proxies = proxies.filter((proxy) => {
|
// https://clash.wiki/configuration/outbound.html#shadowsocks
|
||||||
if (
|
const list = proxies
|
||||||
![
|
.filter((proxy) => {
|
||||||
'ss',
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
'ssr',
|
if (
|
||||||
'vmess',
|
![
|
||||||
'vless',
|
'ss',
|
||||||
'socks5',
|
'ssr',
|
||||||
'http',
|
'vmess',
|
||||||
'snell',
|
'vless',
|
||||||
'trojan',
|
'socks5',
|
||||||
'wireguard',
|
'http',
|
||||||
].includes(proxy.type) ||
|
'snell',
|
||||||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
'trojan',
|
||||||
(proxy.type === 'vless' &&
|
'wireguard',
|
||||||
(typeof proxy.flow !== 'undefined' ||
|
].includes(proxy.type) ||
|
||||||
proxy['reality-opts']))
|
(proxy.type === 'ss' &&
|
||||||
) {
|
![
|
||||||
return false;
|
'aes-128-gcm',
|
||||||
}
|
'aes-192-gcm',
|
||||||
return true;
|
'aes-256-gcm',
|
||||||
});
|
'aes-128-cfb',
|
||||||
return (
|
'aes-192-cfb',
|
||||||
'proxies:\n' +
|
'aes-256-cfb',
|
||||||
proxies
|
'aes-128-ctr',
|
||||||
.map((proxy) => {
|
'aes-192-ctr',
|
||||||
if (proxy.type === 'vmess') {
|
'aes-256-ctr',
|
||||||
// handle vmess aead
|
'rc4-md5',
|
||||||
if (isPresent(proxy, 'aead')) {
|
'chacha20-ietf',
|
||||||
if (proxy.aead) {
|
'xchacha20',
|
||||||
proxy.alterId = 0;
|
'chacha20-ietf-poly1305',
|
||||||
}
|
'xchacha20-ietf-poly1305',
|
||||||
delete proxy.aead;
|
].includes(proxy.cipher)) ||
|
||||||
}
|
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||||
if (isPresent(proxy, 'sni')) {
|
(proxy.type === 'vless' &&
|
||||||
proxy.servername = proxy.sni;
|
(typeof proxy.flow !== 'undefined' ||
|
||||||
delete proxy.sni;
|
proxy['reality-opts']))
|
||||||
}
|
) {
|
||||||
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
|
return false;
|
||||||
if (
|
}
|
||||||
isPresent(proxy, 'cipher') &&
|
return true;
|
||||||
![
|
})
|
||||||
'auto',
|
.map((proxy) => {
|
||||||
'aes-128-gcm',
|
if (proxy.type === 'vmess') {
|
||||||
'chacha20-poly1305',
|
// handle vmess aead
|
||||||
'none',
|
if (isPresent(proxy, 'aead')) {
|
||||||
].includes(proxy.cipher)
|
if (proxy.aead) {
|
||||||
) {
|
proxy.alterId = 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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 === 'snell' && proxy.version < 3) {
|
||||||
|
delete proxy.udp;
|
||||||
|
} 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 (
|
if (
|
||||||
['vmess', 'vless'].includes(proxy.type) &&
|
isPresent(proxy, 'http-opts.path') &&
|
||||||
proxy.network === 'http'
|
!Array.isArray(httpPath)
|
||||||
) {
|
) {
|
||||||
let httpPath = proxy['http-opts']?.path;
|
proxy['http-opts'].path = [httpPath];
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||||
if (
|
if (
|
||||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||||
proxy.type,
|
!Array.isArray(httpHost)
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
delete proxy.tls;
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
|
proxy.network === 'h2'
|
||||||
|
) {
|
||||||
|
let path = proxy['h2-opts']?.path;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.path') &&
|
||||||
|
Array.isArray(path)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].path = path[0];
|
||||||
|
}
|
||||||
|
let host = proxy['h2-opts']?.headers?.host;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||||
|
!Array.isArray(host)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].headers.host = [host];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['plugin-opts']?.tls) {
|
||||||
|
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||||
|
proxy['plugin-opts']['skip-cert-verify'] =
|
||||||
|
proxy['skip-cert-verify'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'trojan',
|
||||||
|
'tuic',
|
||||||
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
|
'juicity',
|
||||||
|
'anytls',
|
||||||
|
].includes(proxy.type)
|
||||||
|
) {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
|
||||||
if (proxy['tls-fingerprint']) {
|
if (proxy['tls-fingerprint']) {
|
||||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||||
|
}
|
||||||
|
delete proxy['tls-fingerprint'];
|
||||||
|
|
||||||
|
if (proxy['underlying-proxy']) {
|
||||||
|
proxy['dialer-proxy'] = proxy['underlying-proxy'];
|
||||||
|
}
|
||||||
|
delete proxy['underlying-proxy'];
|
||||||
|
|
||||||
|
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
delete proxy.id;
|
||||||
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
|
if (type !== 'internal') {
|
||||||
|
for (const key in proxy) {
|
||||||
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
|
delete proxy[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delete proxy['tls-fingerprint'];
|
}
|
||||||
delete proxy.subName;
|
if (
|
||||||
delete proxy.collectionName;
|
['grpc'].includes(proxy.network) &&
|
||||||
if (
|
proxy[`${proxy.network}-opts`]
|
||||||
['grpc'].includes(proxy.network) &&
|
) {
|
||||||
proxy[`${proxy.network}-opts`]
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
) {
|
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
|
||||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
}
|
||||||
}
|
return proxy;
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
});
|
||||||
})
|
return type === 'internal'
|
||||||
.join('')
|
? list
|
||||||
);
|
: 'proxies:\n' +
|
||||||
|
list
|
||||||
|
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||||
|
.join('');
|
||||||
};
|
};
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
|
|
||||||
export default function ClashMeta_Producer() {
|
export default function ClashMeta_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies, type) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
const list = proxies
|
const list = proxies
|
||||||
.filter((proxy) => {
|
.filter((proxy) => {
|
||||||
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (['juicity'].includes(proxy.type)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
@@ -29,9 +32,10 @@ export default function ClashMeta_Producer() {
|
|||||||
isPresent(proxy, 'cipher') &&
|
isPresent(proxy, 'cipher') &&
|
||||||
![
|
![
|
||||||
'auto',
|
'auto',
|
||||||
|
'none',
|
||||||
|
'zero',
|
||||||
'aes-128-gcm',
|
'aes-128-gcm',
|
||||||
'chacha20-poly1305',
|
'chacha20-poly1305',
|
||||||
'none',
|
|
||||||
].includes(proxy.cipher)
|
].includes(proxy.cipher)
|
||||||
) {
|
) {
|
||||||
proxy.cipher = 'auto';
|
proxy.cipher = 'auto';
|
||||||
@@ -83,11 +87,28 @@ export default function ClashMeta_Producer() {
|
|||||||
proxy['preshared-key'] =
|
proxy['preshared-key'] =
|
||||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||||
|
} else if (proxy.type === 'snell' && proxy.version < 3) {
|
||||||
|
delete proxy.udp;
|
||||||
} else if (proxy.type === 'vless') {
|
} else if (proxy.type === 'vless') {
|
||||||
if (isPresent(proxy, 'sni')) {
|
if (isPresent(proxy, 'sni')) {
|
||||||
proxy.servername = proxy.sni;
|
proxy.servername = proxy.sni;
|
||||||
delete proxy.sni;
|
delete proxy.sni;
|
||||||
}
|
}
|
||||||
|
} else if (proxy.type === 'ss') {
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'shadow-tls-password') &&
|
||||||
|
!isPresent(proxy, 'plugin')
|
||||||
|
) {
|
||||||
|
proxy.plugin = 'shadow-tls';
|
||||||
|
proxy['plugin-opts'] = {
|
||||||
|
host: proxy['shadow-tls-sni'],
|
||||||
|
password: proxy['shadow-tls-password'],
|
||||||
|
version: proxy['shadow-tls-version'],
|
||||||
|
};
|
||||||
|
delete proxy['shadow-tls-password'];
|
||||||
|
delete proxy['shadow-tls-sni'];
|
||||||
|
delete proxy['shadow-tls-version'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -109,11 +130,41 @@ export default function ClashMeta_Producer() {
|
|||||||
proxy['http-opts'].headers.Host = [httpHost];
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
proxy.type,
|
proxy.network === 'h2'
|
||||||
)
|
) {
|
||||||
|
let path = proxy['h2-opts']?.path;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.path') &&
|
||||||
|
Array.isArray(path)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].path = path[0];
|
||||||
|
}
|
||||||
|
let host = proxy['h2-opts']?.headers?.host;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||||
|
!Array.isArray(host)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].headers.host = [host];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy['plugin-opts']?.tls) {
|
||||||
|
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||||
|
proxy['plugin-opts']['skip-cert-verify'] =
|
||||||
|
proxy['skip-cert-verify'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'trojan',
|
||||||
|
'tuic',
|
||||||
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
|
'juicity',
|
||||||
|
'anytls',
|
||||||
|
].includes(proxy.type)
|
||||||
) {
|
) {
|
||||||
delete proxy.tls;
|
delete proxy.tls;
|
||||||
}
|
}
|
||||||
@@ -122,13 +173,33 @@ export default function ClashMeta_Producer() {
|
|||||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||||
}
|
}
|
||||||
delete proxy['tls-fingerprint'];
|
delete proxy['tls-fingerprint'];
|
||||||
|
|
||||||
|
if (proxy['underlying-proxy']) {
|
||||||
|
proxy['dialer-proxy'] = proxy['underlying-proxy'];
|
||||||
|
}
|
||||||
|
delete proxy['underlying-proxy'];
|
||||||
|
|
||||||
|
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
delete proxy.subName;
|
delete proxy.subName;
|
||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
|
delete proxy.id;
|
||||||
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
|
if (type !== 'internal' || opts['delete-underscore-fields']) {
|
||||||
|
for (const key in proxy) {
|
||||||
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
|
delete proxy[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
['grpc'].includes(proxy.network) &&
|
['grpc'].includes(proxy.network) &&
|
||||||
proxy[`${proxy.network}-opts`]
|
proxy[`${proxy.network}-opts`]
|
||||||
) {
|
) {
|
||||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
|
||||||
}
|
}
|
||||||
return proxy;
|
return proxy;
|
||||||
});
|
});
|
||||||
|
|||||||
400
backend/src/core/proxy-utils/producers/egern.js
Normal file
400
backend/src/core/proxy-utils/producers/egern.js
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
import { isPresent } from './utils';
|
||||||
|
|
||||||
|
export default function Egern_Producer() {
|
||||||
|
const type = 'ALL';
|
||||||
|
const produce = (proxies, type) => {
|
||||||
|
// https://egernapp.com/zh-CN/docs/configuration/proxies
|
||||||
|
const list = proxies
|
||||||
|
.filter((proxy) => {
|
||||||
|
// if (opts['include-unsupported-proxy']) return true;
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'http',
|
||||||
|
'socks5',
|
||||||
|
'ss',
|
||||||
|
'trojan',
|
||||||
|
'hysteria2',
|
||||||
|
'vless',
|
||||||
|
'vmess',
|
||||||
|
'tuic',
|
||||||
|
].includes(proxy.type) ||
|
||||||
|
(proxy.type === 'ss' &&
|
||||||
|
((proxy.plugin === 'obfs' &&
|
||||||
|
!['http', 'tls'].includes(
|
||||||
|
proxy['plugin-opts']?.mode,
|
||||||
|
)) ||
|
||||||
|
![
|
||||||
|
'chacha20-ietf-poly1305',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'aes-256-gcm',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'none',
|
||||||
|
'tbale',
|
||||||
|
'rc4',
|
||||||
|
'rc4-md5',
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-192-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'aes-128-ctr',
|
||||||
|
'aes-192-ctr',
|
||||||
|
'aes-256-ctr',
|
||||||
|
'bf-cfb',
|
||||||
|
'camellia-128-cfb',
|
||||||
|
'camellia-192-cfb',
|
||||||
|
'camellia-256-cfb',
|
||||||
|
'cast5-cfb',
|
||||||
|
'des-cfb',
|
||||||
|
'idea-cfb',
|
||||||
|
'rc2-cfb',
|
||||||
|
'seed-cfb',
|
||||||
|
'salsa20',
|
||||||
|
'chacha20',
|
||||||
|
'chacha20-ietf',
|
||||||
|
'2022-blake3-aes-128-gcm',
|
||||||
|
'2022-blake3-aes-256-gcm',
|
||||||
|
].includes(proxy.cipher))) ||
|
||||||
|
(proxy.type === 'vmess' &&
|
||||||
|
!['http', 'ws', 'tcp'].includes(proxy.network) &&
|
||||||
|
proxy.network) ||
|
||||||
|
(proxy.type === 'trojan' &&
|
||||||
|
!['http', 'ws', 'tcp'].includes(proxy.network) &&
|
||||||
|
proxy.network) ||
|
||||||
|
(proxy.type === 'vless' &&
|
||||||
|
(typeof proxy.flow !== 'undefined' ||
|
||||||
|
proxy['reality-opts'] ||
|
||||||
|
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
|
||||||
|
proxy.network))) ||
|
||||||
|
(proxy.type === 'tuic' &&
|
||||||
|
proxy.token &&
|
||||||
|
proxy.token.length !== 0)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((proxy) => {
|
||||||
|
const original = { ...proxy };
|
||||||
|
if (proxy.tls && !proxy.sni) {
|
||||||
|
proxy.sni = proxy.server;
|
||||||
|
}
|
||||||
|
const prev_hop =
|
||||||
|
proxy.prev_hop ||
|
||||||
|
proxy['underlying-proxy'] ||
|
||||||
|
proxy['dialer-proxy'] ||
|
||||||
|
proxy.detour;
|
||||||
|
|
||||||
|
if (proxy.type === 'http') {
|
||||||
|
proxy = {
|
||||||
|
type: 'http',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
username: proxy.username,
|
||||||
|
password: proxy.password,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
};
|
||||||
|
} else if (proxy.type === 'socks5') {
|
||||||
|
proxy = {
|
||||||
|
type: 'socks5',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
username: proxy.username,
|
||||||
|
password: proxy.password,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
udp_relay:
|
||||||
|
proxy.udp || proxy.udp_relay || proxy.udp_relay,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
};
|
||||||
|
} else if (proxy.type === 'ss') {
|
||||||
|
proxy = {
|
||||||
|
type: 'shadowsocks',
|
||||||
|
name: proxy.name,
|
||||||
|
method:
|
||||||
|
proxy.cipher === 'chacha20-ietf-poly1305'
|
||||||
|
? 'chacha20-poly1305'
|
||||||
|
: proxy.cipher,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
password: proxy.password,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
udp_relay:
|
||||||
|
proxy.udp || proxy.udp_relay || proxy.udp_relay,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
};
|
||||||
|
if (proxy.plugin === 'obfs') {
|
||||||
|
proxy.obfs = proxy['plugin-opts'].mode;
|
||||||
|
proxy.obfs_host = proxy['plugin-opts'].host;
|
||||||
|
proxy.obfs_uri = proxy['plugin-opts'].path;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'hysteria2') {
|
||||||
|
proxy = {
|
||||||
|
type: 'hysteria2',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
auth: proxy.password,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
udp_relay:
|
||||||
|
proxy.udp || proxy.udp_relay || proxy.udp_relay,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
sni: proxy.sni,
|
||||||
|
skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
port_hopping: proxy.ports,
|
||||||
|
port_hopping_interval: proxy['hop-interval'],
|
||||||
|
};
|
||||||
|
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
|
||||||
|
proxy.obfs = 'salamander';
|
||||||
|
proxy.obfs_password = proxy['obfs-password'];
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'tuic') {
|
||||||
|
proxy = {
|
||||||
|
type: 'tuic',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
password: proxy.password,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
sni: proxy.sni,
|
||||||
|
alpn: Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: [proxy.alpn || 'h3'],
|
||||||
|
skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
port_hopping: proxy.ports,
|
||||||
|
port_hopping_interval: proxy['hop-interval'],
|
||||||
|
};
|
||||||
|
} else if (proxy.type === 'trojan') {
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
proxy.websocket = {
|
||||||
|
path: proxy['ws-opts']?.path,
|
||||||
|
host: proxy['ws-opts']?.headers?.Host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
proxy = {
|
||||||
|
type: 'trojan',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
password: proxy.password,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
udp_relay:
|
||||||
|
proxy.udp || proxy.udp_relay || proxy.udp_relay,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
sni: proxy.sni,
|
||||||
|
skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
websocket: proxy.websocket,
|
||||||
|
};
|
||||||
|
} else if (proxy.type === 'vmess') {
|
||||||
|
// Egern:传输层,支持 ws/wss/http1/http2/tls,不配置则为 tcp
|
||||||
|
let security = proxy.cipher;
|
||||||
|
if (
|
||||||
|
security &&
|
||||||
|
![
|
||||||
|
'auto',
|
||||||
|
'none',
|
||||||
|
'zero',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
].includes(security)
|
||||||
|
) {
|
||||||
|
security = 'auto';
|
||||||
|
}
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
proxy.transport = {
|
||||||
|
[proxy.tls ? 'wss' : 'ws']: {
|
||||||
|
path: proxy['ws-opts']?.path,
|
||||||
|
headers: {
|
||||||
|
Host: proxy['ws-opts']?.headers?.Host,
|
||||||
|
},
|
||||||
|
sni: proxy.tls ? proxy.sni : undefined,
|
||||||
|
skip_tls_verify: proxy.tls
|
||||||
|
? proxy['skip-cert-verify']
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (proxy.network === 'http') {
|
||||||
|
proxy.transport = {
|
||||||
|
http1: {
|
||||||
|
method: proxy['http-opts']?.method,
|
||||||
|
path: Array.isArray(proxy['http-opts']?.path)
|
||||||
|
? proxy['http-opts']?.path[0]
|
||||||
|
: proxy['http-opts']?.path,
|
||||||
|
headers: {
|
||||||
|
Host: Array.isArray(
|
||||||
|
proxy['http-opts']?.headers?.Host,
|
||||||
|
)
|
||||||
|
? proxy['http-opts']?.headers?.Host[0]
|
||||||
|
: proxy['http-opts']?.headers?.Host,
|
||||||
|
},
|
||||||
|
skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (proxy.network === 'h2') {
|
||||||
|
proxy.transport = {
|
||||||
|
http2: {
|
||||||
|
method: proxy['h2-opts']?.method,
|
||||||
|
path: Array.isArray(proxy['h2-opts']?.path)
|
||||||
|
? proxy['h2-opts']?.path[0]
|
||||||
|
: proxy['h2-opts']?.path,
|
||||||
|
headers: {
|
||||||
|
Host: Array.isArray(
|
||||||
|
proxy['h2-opts']?.headers?.Host,
|
||||||
|
)
|
||||||
|
? proxy['h2-opts']?.headers?.Host[0]
|
||||||
|
: proxy['h2-opts']?.headers?.Host,
|
||||||
|
},
|
||||||
|
skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
(proxy.network === 'tcp' || !proxy.network) &&
|
||||||
|
proxy.tls
|
||||||
|
) {
|
||||||
|
proxy.transport = {
|
||||||
|
tls: {
|
||||||
|
sni: proxy.tls ? proxy.sni : undefined,
|
||||||
|
skip_tls_verify: proxy.tls
|
||||||
|
? proxy['skip-cert-verify']
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
proxy = {
|
||||||
|
type: 'vmess',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
user_id: proxy.uuid,
|
||||||
|
security,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
legacy: proxy.legacy,
|
||||||
|
udp_relay:
|
||||||
|
proxy.udp || proxy.udp_relay || proxy.udp_relay,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
transport: proxy.transport,
|
||||||
|
// sni: proxy.sni,
|
||||||
|
// skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
};
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
proxy.transport = {
|
||||||
|
[proxy.tls ? 'wss' : 'ws']: {
|
||||||
|
path: proxy['ws-opts']?.path,
|
||||||
|
headers: {
|
||||||
|
Host: proxy['ws-opts']?.headers?.Host,
|
||||||
|
},
|
||||||
|
sni: proxy.tls ? proxy.sni : undefined,
|
||||||
|
skip_tls_verify: proxy.tls
|
||||||
|
? proxy['skip-cert-verify']
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (proxy.network === 'http') {
|
||||||
|
proxy.transport = {
|
||||||
|
http: {
|
||||||
|
method: proxy['http-opts']?.method,
|
||||||
|
path: Array.isArray(proxy['http-opts']?.path)
|
||||||
|
? proxy['http-opts']?.path[0]
|
||||||
|
: proxy['http-opts']?.path,
|
||||||
|
headers: {
|
||||||
|
Host: Array.isArray(
|
||||||
|
proxy['http-opts']?.headers?.Host,
|
||||||
|
)
|
||||||
|
? proxy['http-opts']?.headers?.Host[0]
|
||||||
|
: proxy['http-opts']?.headers?.Host,
|
||||||
|
},
|
||||||
|
skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (proxy.network === 'tcp' || !proxy.network) {
|
||||||
|
proxy.transport = {
|
||||||
|
[proxy.tls ? 'tls' : 'tcp']: {
|
||||||
|
sni: proxy.tls ? proxy.sni : undefined,
|
||||||
|
skip_tls_verify: proxy.tls
|
||||||
|
? proxy['skip-cert-verify']
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
proxy = {
|
||||||
|
type: 'vless',
|
||||||
|
name: proxy.name,
|
||||||
|
server: proxy.server,
|
||||||
|
port: proxy.port,
|
||||||
|
user_id: proxy.uuid,
|
||||||
|
security: proxy.cipher,
|
||||||
|
tfo: proxy.tfo || proxy['fast-open'],
|
||||||
|
legacy: proxy.legacy,
|
||||||
|
udp_relay:
|
||||||
|
proxy.udp || proxy.udp_relay || proxy.udp_relay,
|
||||||
|
next_hop: proxy.next_hop,
|
||||||
|
transport: proxy.transport,
|
||||||
|
// sni: proxy.sni,
|
||||||
|
// skip_tls_verify: proxy['skip-cert-verify'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'http',
|
||||||
|
'socks5',
|
||||||
|
'ss',
|
||||||
|
'trojan',
|
||||||
|
'vless',
|
||||||
|
'vmess',
|
||||||
|
].includes(original.type)
|
||||||
|
) {
|
||||||
|
if (isPresent(original, 'shadow-tls-password')) {
|
||||||
|
if (original['shadow-tls-version'] != 3)
|
||||||
|
throw new Error(
|
||||||
|
`shadow-tls version ${original['shadow-tls-version']} is not supported`,
|
||||||
|
);
|
||||||
|
proxy.shadow_tls = {
|
||||||
|
password: original['shadow-tls-password'],
|
||||||
|
sni: original['shadow-tls-sni'],
|
||||||
|
};
|
||||||
|
} else if (
|
||||||
|
['shadow-tls'].includes(original.plugin) &&
|
||||||
|
original['plugin-opts']
|
||||||
|
) {
|
||||||
|
if (original['plugin-opts'].version != 3)
|
||||||
|
throw new Error(
|
||||||
|
`shadow-tls version ${original['plugin-opts'].version} is not supported`,
|
||||||
|
);
|
||||||
|
proxy.shadow_tls = {
|
||||||
|
password: original['plugin-opts'].password,
|
||||||
|
sni: original['plugin-opts'].host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
delete proxy.id;
|
||||||
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
|
if (type !== 'internal') {
|
||||||
|
for (const key in proxy) {
|
||||||
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
|
delete proxy[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[proxy.type]: {
|
||||||
|
...proxy,
|
||||||
|
type: undefined,
|
||||||
|
prev_hop,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return type === 'internal'
|
||||||
|
? list
|
||||||
|
: 'proxies:\n' +
|
||||||
|
list
|
||||||
|
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
return { type, produce };
|
||||||
|
}
|
||||||
@@ -7,24 +7,50 @@ import Loon_Producer from './loon';
|
|||||||
import URI_Producer from './uri';
|
import URI_Producer from './uri';
|
||||||
import V2Ray_Producer from './v2ray';
|
import V2Ray_Producer from './v2ray';
|
||||||
import QX_Producer from './qx';
|
import QX_Producer from './qx';
|
||||||
import ShadowRocket_Producer from './shadowrocket';
|
import Shadowrocket_Producer from './shadowrocket';
|
||||||
|
import Surfboard_Producer from './surfboard';
|
||||||
|
import singbox_Producer from './sing-box';
|
||||||
|
import Egern_Producer from './egern';
|
||||||
|
|
||||||
function JSON_Producer() {
|
function JSON_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => JSON.stringify(proxies, null, 2);
|
const produce = (proxies, type) =>
|
||||||
|
type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
qx: QX_Producer(),
|
||||||
QX: QX_Producer(),
|
QX: QX_Producer(),
|
||||||
|
QuantumultX: QX_Producer(),
|
||||||
|
surge: Surge_Producer(),
|
||||||
Surge: Surge_Producer(),
|
Surge: Surge_Producer(),
|
||||||
SurgeMac: SurgeMac_Producer(),
|
SurgeMac: SurgeMac_Producer(),
|
||||||
Loon: Loon_Producer(),
|
Loon: Loon_Producer(),
|
||||||
Clash: Clash_Producer(),
|
Clash: Clash_Producer(),
|
||||||
|
meta: ClashMeta_Producer(),
|
||||||
|
clashmeta: ClashMeta_Producer(),
|
||||||
|
'clash.meta': ClashMeta_Producer(),
|
||||||
|
'Clash.Meta': ClashMeta_Producer(),
|
||||||
ClashMeta: ClashMeta_Producer(),
|
ClashMeta: ClashMeta_Producer(),
|
||||||
|
mihomo: ClashMeta_Producer(),
|
||||||
|
Mihomo: ClashMeta_Producer(),
|
||||||
|
uri: URI_Producer(),
|
||||||
URI: URI_Producer(),
|
URI: URI_Producer(),
|
||||||
|
v2: V2Ray_Producer(),
|
||||||
|
v2ray: V2Ray_Producer(),
|
||||||
V2Ray: V2Ray_Producer(),
|
V2Ray: V2Ray_Producer(),
|
||||||
|
json: JSON_Producer(),
|
||||||
JSON: JSON_Producer(),
|
JSON: JSON_Producer(),
|
||||||
|
stash: Stash_Producer(),
|
||||||
Stash: Stash_Producer(),
|
Stash: Stash_Producer(),
|
||||||
ShadowRocket: ShadowRocket_Producer(),
|
shadowrocket: Shadowrocket_Producer(),
|
||||||
|
Shadowrocket: Shadowrocket_Producer(),
|
||||||
|
ShadowRocket: Shadowrocket_Producer(),
|
||||||
|
surfboard: Surfboard_Producer(),
|
||||||
|
Surfboard: Surfboard_Producer(),
|
||||||
|
singbox: singbox_Producer(),
|
||||||
|
'sing-box': singbox_Producer(),
|
||||||
|
egern: Egern_Producer(),
|
||||||
|
Egern: Egern_Producer(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,16 @@ const targetPlatform = 'Loon';
|
|||||||
import { isPresent, Result } from './utils';
|
import { isPresent, Result } from './utils';
|
||||||
import { isIPv4, isIPv6 } from '@/utils';
|
import { isIPv4, isIPv6 } from '@/utils';
|
||||||
|
|
||||||
|
const ipVersions = {
|
||||||
|
dual: 'dual',
|
||||||
|
ipv4: 'v4-only',
|
||||||
|
ipv6: 'v6-only',
|
||||||
|
'ipv4-prefer': 'prefer-v4',
|
||||||
|
'ipv6-prefer': 'prefer-v6',
|
||||||
|
};
|
||||||
|
|
||||||
export default function Loon_Producer() {
|
export default function Loon_Producer() {
|
||||||
const produce = (proxy) => {
|
const produce = (proxy, type, opts = {}) => {
|
||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'ss':
|
case 'ss':
|
||||||
return shadowsocks(proxy);
|
return shadowsocks(proxy);
|
||||||
@@ -18,6 +26,8 @@ export default function Loon_Producer() {
|
|||||||
return vless(proxy);
|
return vless(proxy);
|
||||||
case 'http':
|
case 'http':
|
||||||
return http(proxy);
|
return http(proxy);
|
||||||
|
case 'socks5':
|
||||||
|
return socks5(proxy);
|
||||||
case 'wireguard':
|
case 'wireguard':
|
||||||
return wireguard(proxy);
|
return wireguard(proxy);
|
||||||
case 'hysteria2':
|
case 'hysteria2':
|
||||||
@@ -32,6 +42,34 @@ export default function Loon_Producer() {
|
|||||||
|
|
||||||
function shadowsocks(proxy) {
|
function shadowsocks(proxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'rc4',
|
||||||
|
'rc4-md5',
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-192-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'aes-128-ctr',
|
||||||
|
'aes-192-ctr',
|
||||||
|
'aes-256-ctr',
|
||||||
|
'bf-cfb',
|
||||||
|
'camellia-128-cfb',
|
||||||
|
'camellia-192-cfb',
|
||||||
|
'camellia-256-cfb',
|
||||||
|
'salsa20',
|
||||||
|
'chacha20',
|
||||||
|
'chacha20-ietf',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'aes-192-gcm',
|
||||||
|
'aes-256-gcm',
|
||||||
|
'chacha20-ietf-poly1305',
|
||||||
|
'xchacha20-ietf-poly1305',
|
||||||
|
'2022-blake3-aes-128-gcm',
|
||||||
|
'2022-blake3-aes-256-gcm',
|
||||||
|
].includes(proxy.cipher)
|
||||||
|
) {
|
||||||
|
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||||
|
}
|
||||||
result.append(
|
result.append(
|
||||||
`${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
`${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
||||||
);
|
);
|
||||||
@@ -48,21 +86,65 @@ function shadowsocks(proxy) {
|
|||||||
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||||
'plugin-opts.path',
|
'plugin-opts.path',
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!['shadow-tls'].includes(proxy.plugin)) {
|
||||||
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
);
|
||||||
|
// udp-port
|
||||||
|
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
|
||||||
|
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
|
||||||
|
const password = proxy['plugin-opts'].password;
|
||||||
|
const host = proxy['plugin-opts'].host;
|
||||||
|
const version = proxy['plugin-opts'].version;
|
||||||
|
if (password) {
|
||||||
|
result.append(`,shadow-tls-password=${password}`);
|
||||||
|
if (host) {
|
||||||
|
result.append(`,shadow-tls-sni=${host}`);
|
||||||
|
}
|
||||||
|
if (version) {
|
||||||
|
if (version < 2) {
|
||||||
|
throw new Error(
|
||||||
|
`shadow-tls version ${version} is not supported`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.append(`,shadow-tls-version=${version}`);
|
||||||
|
}
|
||||||
|
// udp-port
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,udp-port=${proxy['udp-port']}`,
|
||||||
|
'udp-port',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// tfo
|
// tfo
|
||||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function shadowsocksr(proxy) {
|
function shadowsocksr(proxy, includeUnsupportedProxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(
|
result.append(
|
||||||
`${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
`${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
||||||
@@ -79,11 +161,55 @@ function shadowsocksr(proxy) {
|
|||||||
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
|
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
|
||||||
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
|
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
);
|
||||||
|
// udp-port
|
||||||
|
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
|
||||||
|
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
|
||||||
|
const password = proxy['plugin-opts'].password;
|
||||||
|
const host = proxy['plugin-opts'].host;
|
||||||
|
const version = proxy['plugin-opts'].version;
|
||||||
|
if (password) {
|
||||||
|
result.append(`,shadow-tls-password=${password}`);
|
||||||
|
if (host) {
|
||||||
|
result.append(`,shadow-tls-sni=${host}`);
|
||||||
|
}
|
||||||
|
if (version) {
|
||||||
|
if (version < 2) {
|
||||||
|
throw new Error(
|
||||||
|
`shadow-tls version ${version} is not supported`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.append(`,shadow-tls-version=${version}`);
|
||||||
|
}
|
||||||
|
// udp-port
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,udp-port=${proxy['udp-port']}`,
|
||||||
|
'udp-port',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// tfo
|
// tfo
|
||||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
@@ -93,7 +219,9 @@ function trojan(proxy) {
|
|||||||
result.append(
|
result.append(
|
||||||
`${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
|
`${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
|
||||||
);
|
);
|
||||||
|
if (proxy.network === 'tcp') {
|
||||||
|
delete proxy.network;
|
||||||
|
}
|
||||||
// transport
|
// transport
|
||||||
if (isPresent(proxy, 'network')) {
|
if (isPresent(proxy, 'network')) {
|
||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
@@ -119,12 +247,24 @@ function trojan(proxy) {
|
|||||||
|
|
||||||
// sni
|
// sni
|
||||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||||
|
'tls-fingerprint',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||||
|
'tls-pubkey-sha256',
|
||||||
|
);
|
||||||
|
|
||||||
// tfo
|
// tfo
|
||||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
@@ -134,7 +274,9 @@ function vmess(proxy) {
|
|||||||
result.append(
|
result.append(
|
||||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
||||||
);
|
);
|
||||||
|
if (proxy.network === 'tcp') {
|
||||||
|
delete proxy.network;
|
||||||
|
}
|
||||||
// transport
|
// transport
|
||||||
if (isPresent(proxy, 'network')) {
|
if (isPresent(proxy, 'network')) {
|
||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
@@ -177,6 +319,14 @@ function vmess(proxy) {
|
|||||||
|
|
||||||
// sni
|
// sni
|
||||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||||
|
'tls-fingerprint',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||||
|
'tls-pubkey-sha256',
|
||||||
|
);
|
||||||
|
|
||||||
// AEAD
|
// AEAD
|
||||||
if (isPresent(proxy, 'aead')) {
|
if (isPresent(proxy, 'aead')) {
|
||||||
@@ -189,19 +339,25 @@ function vmess(proxy) {
|
|||||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function vless(proxy) {
|
function vless(proxy) {
|
||||||
if (proxy['reality-opts']) {
|
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) {
|
||||||
throw new Error(`reality is unsupported`);
|
throw new Error(`VLESS XTLS/REALITY is not supported`);
|
||||||
}
|
}
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(
|
result.append(
|
||||||
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
||||||
);
|
);
|
||||||
|
if (proxy.network === 'tcp') {
|
||||||
|
delete proxy.network;
|
||||||
|
}
|
||||||
// transport
|
// transport
|
||||||
if (isPresent(proxy, 'network')) {
|
if (isPresent(proxy, 'network')) {
|
||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
@@ -244,12 +400,24 @@ function vless(proxy) {
|
|||||||
|
|
||||||
// sni
|
// sni
|
||||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||||
|
'tls-fingerprint',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||||
|
'tls-pubkey-sha256',
|
||||||
|
);
|
||||||
|
|
||||||
// tfo
|
// tfo
|
||||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +437,41 @@ function http(proxy) {
|
|||||||
'skip-cert-verify',
|
'skip-cert-verify',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
function socks5(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=socks5,${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
|
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||||
|
|
||||||
|
// tls
|
||||||
|
result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||||
|
|
||||||
|
// sni
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
// tfo
|
// tfo
|
||||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +483,8 @@ function wireguard(proxy) {
|
|||||||
proxy.ipv6 = proxy.peers[0].ipv6;
|
proxy.ipv6 = proxy.peers[0].ipv6;
|
||||||
proxy['public-key'] = proxy.peers[0]['public-key'];
|
proxy['public-key'] = proxy.peers[0]['public-key'];
|
||||||
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
|
||||||
proxy['allowed-ips'] = proxy.peers[0]['allowed_ips'];
|
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||||
|
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
|
||||||
proxy.reserved = proxy.peers[0].reserved;
|
proxy.reserved = proxy.peers[0].reserved;
|
||||||
}
|
}
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
@@ -303,7 +502,11 @@ function wireguard(proxy) {
|
|||||||
if (proxy.dns) {
|
if (proxy.dns) {
|
||||||
if (Array.isArray(proxy.dns)) {
|
if (Array.isArray(proxy.dns)) {
|
||||||
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
|
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
|
||||||
proxy.dns = proxy.dns.find((i) => isIPv4(i));
|
let dns = proxy.dns.find((i) => isIPv4(i));
|
||||||
|
if (!dns) {
|
||||||
|
dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i));
|
||||||
|
}
|
||||||
|
proxy.dns = dns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
|
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
|
||||||
@@ -333,13 +536,15 @@ function wireguard(proxy) {
|
|||||||
presharedKey ?? ''
|
presharedKey ?? ''
|
||||||
}}]`,
|
}}]`,
|
||||||
);
|
);
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hysteria2(proxy) {
|
function hysteria2(proxy) {
|
||||||
if (proxy.obfs || proxy['obfs-password']) {
|
if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
|
||||||
throw new Error(`obfs is unsupported`);
|
throw new Error(`only salamander obfs is supported`);
|
||||||
}
|
}
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
|
||||||
@@ -348,13 +553,30 @@ function hysteria2(proxy) {
|
|||||||
|
|
||||||
// sni
|
// sni
|
||||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||||
|
'tls-fingerprint',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||||
|
'tls-pubkey-sha256',
|
||||||
|
);
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
'skip-cert-verify',
|
'skip-cert-verify',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
|
||||||
|
result.append(`,salamander-password=${proxy['obfs-password']}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
|
if (proxy.udp) {
|
||||||
|
result.append(`,udp=true`);
|
||||||
|
}
|
||||||
|
|
||||||
// download-bandwidth
|
// download-bandwidth
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
@@ -363,6 +585,8 @@ function hysteria2(proxy) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { isPresent, Result } from './utils';
|
|||||||
const targetPlatform = 'QX';
|
const targetPlatform = 'QX';
|
||||||
|
|
||||||
export default function QX_Producer() {
|
export default function QX_Producer() {
|
||||||
const produce = (proxy) => {
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const produce = (proxy, type, opts = {}) => {
|
||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'ss':
|
case 'ss':
|
||||||
return shadowsocks(proxy);
|
return shadowsocks(proxy);
|
||||||
@@ -17,6 +18,8 @@ export default function QX_Producer() {
|
|||||||
return http(proxy);
|
return http(proxy);
|
||||||
case 'socks5':
|
case 'socks5':
|
||||||
return socks5(proxy);
|
return socks5(proxy);
|
||||||
|
case 'vless':
|
||||||
|
return vless(proxy);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||||
@@ -29,7 +32,36 @@ function shadowsocks(proxy) {
|
|||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
const append = result.append.bind(result);
|
const append = result.append.bind(result);
|
||||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||||
|
if (!proxy.cipher) {
|
||||||
|
proxy.cipher = 'none';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'none',
|
||||||
|
'rc4-md5',
|
||||||
|
'rc4-md5-6',
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-192-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'aes-128-ctr',
|
||||||
|
'aes-192-ctr',
|
||||||
|
'aes-256-ctr',
|
||||||
|
'bf-cfb',
|
||||||
|
'cast5-cfb',
|
||||||
|
'des-cfb',
|
||||||
|
'rc2-cfb',
|
||||||
|
'salsa20',
|
||||||
|
'chacha20',
|
||||||
|
'chacha20-ietf',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'aes-192-gcm',
|
||||||
|
'aes-256-gcm',
|
||||||
|
'chacha20-ietf-poly1305',
|
||||||
|
'xchacha20-ietf-poly1305',
|
||||||
|
].includes(proxy.cipher)
|
||||||
|
) {
|
||||||
|
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||||
|
}
|
||||||
append(`shadowsocks=${proxy.server}:${proxy.port}`);
|
append(`shadowsocks=${proxy.server}:${proxy.port}`);
|
||||||
append(`,method=${proxy.cipher}`);
|
append(`,method=${proxy.cipher}`);
|
||||||
append(`,password=${proxy.password}`);
|
append(`,password=${proxy.password}`);
|
||||||
@@ -325,6 +357,105 @@ function vmess(proxy) {
|
|||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
function vless(proxy) {
|
||||||
|
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) {
|
||||||
|
throw new Error(`VLESS XTLS/REALITY is not supported`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new Result(proxy);
|
||||||
|
const append = result.append.bind(result);
|
||||||
|
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||||
|
|
||||||
|
append(`vless=${proxy.server}:${proxy.port}`);
|
||||||
|
|
||||||
|
// The method field for vless should be none.
|
||||||
|
let cipher = 'none';
|
||||||
|
// if (proxy.cipher === 'auto') {
|
||||||
|
// cipher = 'chacha20-ietf-poly1305';
|
||||||
|
// } else {
|
||||||
|
// cipher = proxy.cipher;
|
||||||
|
// }
|
||||||
|
append(`,method=${cipher}`);
|
||||||
|
|
||||||
|
append(`,password=${proxy.uuid}`);
|
||||||
|
|
||||||
|
// obfs
|
||||||
|
if (needTls(proxy)) {
|
||||||
|
proxy.tls = true;
|
||||||
|
}
|
||||||
|
if (isPresent(proxy, 'network')) {
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
if (proxy.tls) append(`,obfs=wss`);
|
||||||
|
else append(`,obfs=ws`);
|
||||||
|
} else if (proxy.network === 'http') {
|
||||||
|
append(`,obfs=http`);
|
||||||
|
} else if (!['tcp'].includes(proxy.network)) {
|
||||||
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
|
}
|
||||||
|
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||||
|
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||||
|
appendIfPresent(
|
||||||
|
`,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 {
|
||||||
|
// over-tls
|
||||||
|
if (proxy.tls) append(`,obfs=over-tls`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
function http(proxy) {
|
function http(proxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
|
|||||||
@@ -1,163 +1,231 @@
|
|||||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||||
|
|
||||||
export default function ShadowRocket_Producer() {
|
export default function Shadowrocket_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
return (
|
const list = proxies
|
||||||
'proxies:\n' +
|
.filter((proxy) => {
|
||||||
proxies
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
.filter((proxy) => {
|
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||||
|
return false;
|
||||||
|
} else if (['mieru', 'anytls'].includes(proxy.type)) {
|
||||||
|
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 (
|
if (
|
||||||
proxy.type === 'snell' &&
|
isPresent(proxy, 'cipher') &&
|
||||||
String(proxy.version) === '4'
|
![
|
||||||
|
'auto',
|
||||||
|
'none',
|
||||||
|
'zero',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
].includes(proxy.cipher)
|
||||||
) {
|
) {
|
||||||
return false;
|
proxy.cipher = 'auto';
|
||||||
}
|
}
|
||||||
return true;
|
} else if (proxy.type === 'tuic') {
|
||||||
})
|
if (isPresent(proxy, 'alpn')) {
|
||||||
.map((proxy) => {
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
if (proxy.type === 'vmess') {
|
? proxy.alpn
|
||||||
// handle vmess aead
|
: [proxy.alpn];
|
||||||
if (isPresent(proxy, 'aead')) {
|
} else {
|
||||||
if (proxy.aead) {
|
proxy.alpn = ['h3'];
|
||||||
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 (
|
||||||
|
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 === 'snell' && proxy.version < 3) {
|
||||||
|
delete proxy.udp;
|
||||||
|
} else if (proxy.type === 'vless') {
|
||||||
|
if (isPresent(proxy, 'sni')) {
|
||||||
|
proxy.servername = proxy.sni;
|
||||||
|
delete proxy.sni;
|
||||||
|
}
|
||||||
|
} else if (proxy.type === 'ss') {
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'shadow-tls-password') &&
|
||||||
|
!isPresent(proxy, 'plugin')
|
||||||
|
) {
|
||||||
|
proxy.plugin = 'shadow-tls';
|
||||||
|
proxy['plugin-opts'] = {
|
||||||
|
host: proxy['shadow-tls-sni'],
|
||||||
|
password: proxy['shadow-tls-password'],
|
||||||
|
version: proxy['shadow-tls-version'],
|
||||||
|
};
|
||||||
|
delete proxy['shadow-tls-password'];
|
||||||
|
delete proxy['shadow-tls-sni'];
|
||||||
|
delete proxy['shadow-tls-version'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
|
proxy.network === 'http'
|
||||||
|
) {
|
||||||
|
let httpPath = proxy['http-opts']?.path;
|
||||||
if (
|
if (
|
||||||
['vmess', 'vless'].includes(proxy.type) &&
|
isPresent(proxy, 'http-opts.path') &&
|
||||||
proxy.network === 'http'
|
!Array.isArray(httpPath)
|
||||||
) {
|
) {
|
||||||
let httpPath = proxy['http-opts']?.path;
|
proxy['http-opts'].path = [httpPath];
|
||||||
if (
|
}
|
||||||
isPresent(proxy, 'http-opts.path') &&
|
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||||
!Array.isArray(httpPath)
|
if (
|
||||||
) {
|
isPresent(proxy, 'http-opts.headers.Host') &&
|
||||||
proxy['http-opts'].path = [httpPath];
|
!Array.isArray(httpHost)
|
||||||
}
|
) {
|
||||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
proxy['http-opts'].headers.Host = [httpHost];
|
||||||
if (
|
}
|
||||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
}
|
||||||
!Array.isArray(httpHost)
|
if (
|
||||||
) {
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
proxy['http-opts'].headers.Host = [httpHost];
|
proxy.network === 'h2'
|
||||||
|
) {
|
||||||
|
let path = proxy['h2-opts']?.path;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.path') &&
|
||||||
|
Array.isArray(path)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].path = path[0];
|
||||||
|
}
|
||||||
|
let host = proxy['h2-opts']?.headers?.host;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||||
|
!Array.isArray(host)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].headers.host = [host];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['plugin-opts']?.tls) {
|
||||||
|
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||||
|
proxy['plugin-opts']['skip-cert-verify'] =
|
||||||
|
proxy['skip-cert-verify'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'trojan',
|
||||||
|
'tuic',
|
||||||
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
|
'juicity',
|
||||||
|
'anytls',
|
||||||
|
].includes(proxy.type)
|
||||||
|
) {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy['tls-fingerprint']) {
|
||||||
|
proxy.fingerprint = proxy['tls-fingerprint'];
|
||||||
|
}
|
||||||
|
delete proxy['tls-fingerprint'];
|
||||||
|
|
||||||
|
if (proxy['underlying-proxy']) {
|
||||||
|
proxy['dialer-proxy'] = proxy['underlying-proxy'];
|
||||||
|
}
|
||||||
|
delete proxy['underlying-proxy'];
|
||||||
|
|
||||||
|
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
delete proxy.id;
|
||||||
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
|
if (type !== 'internal') {
|
||||||
|
for (const key in proxy) {
|
||||||
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
|
delete proxy[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
['grpc'].includes(proxy.network) &&
|
||||||
proxy.type,
|
proxy[`${proxy.network}-opts`]
|
||||||
)
|
) {
|
||||||
) {
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
delete proxy.tls;
|
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
|
||||||
}
|
}
|
||||||
|
return proxy;
|
||||||
if (proxy['tls-fingerprint']) {
|
});
|
||||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
return type === 'internal'
|
||||||
}
|
? list
|
||||||
delete proxy['tls-fingerprint'];
|
: 'proxies:\n' +
|
||||||
delete proxy.subName;
|
list
|
||||||
delete proxy.collectionName;
|
.map((proxy) => {
|
||||||
if (
|
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||||
['grpc'].includes(proxy.network) &&
|
})
|
||||||
proxy[`${proxy.network}-opts`]
|
.join('');
|
||||||
) {
|
|
||||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
|
||||||
}
|
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|||||||
868
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
868
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
@@ -0,0 +1,868 @@
|
|||||||
|
import ClashMeta_Producer from './clashmeta';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import { isIPv4, isIPv6 } from '@/utils';
|
||||||
|
|
||||||
|
const detourParser = (proxy, parsedProxy) => {
|
||||||
|
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
|
||||||
|
};
|
||||||
|
const networkParser = (proxy, parsedProxy) => {
|
||||||
|
if (['tcp', 'udp'].includes(proxy._network))
|
||||||
|
parsedProxy.network = proxy._network;
|
||||||
|
};
|
||||||
|
const tfoParser = (proxy, parsedProxy) => {
|
||||||
|
parsedProxy.tcp_fast_open = false;
|
||||||
|
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
|
||||||
|
if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
|
||||||
|
if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
|
||||||
|
if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
|
||||||
|
};
|
||||||
|
|
||||||
|
const smuxParser = (smux, proxy) => {
|
||||||
|
if (!smux || !smux.enabled) return;
|
||||||
|
proxy.multiplex = { enabled: true };
|
||||||
|
proxy.multiplex.protocol = smux.protocol;
|
||||||
|
if (smux['max-connections'])
|
||||||
|
proxy.multiplex.max_connections = parseInt(
|
||||||
|
`${smux['max-connections']}`,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
if (smux['max-streams'])
|
||||||
|
proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
|
||||||
|
if (smux['min-streams'])
|
||||||
|
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
|
||||||
|
if (smux.padding) proxy.multiplex.padding = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wsParser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'ws', headers: {} };
|
||||||
|
if (proxy['ws-opts']) {
|
||||||
|
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
|
||||||
|
if (wsPath !== '') transport.path = `${wsPath}`;
|
||||||
|
if (Object.keys(wsHeaders).length > 0) {
|
||||||
|
const headers = {};
|
||||||
|
for (const key of Object.keys(wsHeaders)) {
|
||||||
|
let value = wsHeaders[key];
|
||||||
|
if (value === '') continue;
|
||||||
|
if (!Array.isArray(value)) value = [`${value}`];
|
||||||
|
if (value.length > 0) headers[key] = value;
|
||||||
|
}
|
||||||
|
const { Host: wsHost } = headers;
|
||||||
|
if (wsHost.length === 1)
|
||||||
|
for (const item of `Host:${wsHost[0]}`.split('\n')) {
|
||||||
|
const [key, value] = item.split(':');
|
||||||
|
if (value.trim() === '') continue;
|
||||||
|
headers[key.trim()] = value.trim().split(',');
|
||||||
|
}
|
||||||
|
transport.headers = headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['ws-headers']) {
|
||||||
|
const headers = {};
|
||||||
|
for (const key of Object.keys(proxy['ws-headers'])) {
|
||||||
|
let value = proxy['ws-headers'][key];
|
||||||
|
if (value === '') continue;
|
||||||
|
if (!Array.isArray(value)) value = [`${value}`];
|
||||||
|
if (value.length > 0) headers[key] = value;
|
||||||
|
}
|
||||||
|
const { Host: wsHost } = headers;
|
||||||
|
if (wsHost.length === 1)
|
||||||
|
for (const item of `Host:${wsHost[0]}`.split('\n')) {
|
||||||
|
const [key, value] = item.split(':');
|
||||||
|
if (value.trim() === '') continue;
|
||||||
|
headers[key.trim()] = value.trim().split(',');
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(headers))
|
||||||
|
transport.headers[key] = headers[key];
|
||||||
|
}
|
||||||
|
if (proxy['ws-path'] && proxy['ws-path'] !== '')
|
||||||
|
transport.path = `${proxy['ws-path']}`;
|
||||||
|
if (transport.path) {
|
||||||
|
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const [_, path = '', ed = ''] = reg.exec(transport.path);
|
||||||
|
transport.path = path;
|
||||||
|
if (ed !== '') {
|
||||||
|
transport.early_data_header_name = 'Sec-WebSocket-Protocol';
|
||||||
|
transport.max_early_data = parseInt(ed, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedProxy.tls.insecure)
|
||||||
|
parsedProxy.tls.server_name = transport.headers.Host[0];
|
||||||
|
if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
|
||||||
|
transport.type = 'httpupgrade';
|
||||||
|
if (transport.headers.Host) {
|
||||||
|
transport.host = transport.headers.Host[0];
|
||||||
|
delete transport.headers.Host;
|
||||||
|
}
|
||||||
|
if (transport.max_early_data) delete transport.max_early_data;
|
||||||
|
if (transport.early_data_header_name)
|
||||||
|
delete transport.early_data_header_name;
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(transport.headers)) {
|
||||||
|
const value = transport.headers[key];
|
||||||
|
if (value.length === 1) transport.headers[key] = value[0];
|
||||||
|
}
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const h1Parser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'http', headers: {} };
|
||||||
|
if (proxy['http-opts']) {
|
||||||
|
const {
|
||||||
|
method = '',
|
||||||
|
path: h1Path = '',
|
||||||
|
headers: h1Headers = {},
|
||||||
|
} = proxy['http-opts'];
|
||||||
|
if (method !== '') transport.method = method;
|
||||||
|
if (Array.isArray(h1Path)) {
|
||||||
|
transport.path = `${h1Path[0]}`;
|
||||||
|
} else if (h1Path !== '') transport.path = `${h1Path}`;
|
||||||
|
for (const key of Object.keys(h1Headers)) {
|
||||||
|
let value = h1Headers[key];
|
||||||
|
if (value === '') continue;
|
||||||
|
if (key.toLowerCase() === 'host') {
|
||||||
|
let host = value;
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value))
|
||||||
|
value = `${value}`.split(',').map((i) => i.trim());
|
||||||
|
if (value.length > 0) transport.headers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['http-host'] && proxy['http-host'] !== '') {
|
||||||
|
let host = proxy['http-host'];
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
}
|
||||||
|
if (!transport.host) return;
|
||||||
|
if (proxy['http-path'] && proxy['http-path'] !== '') {
|
||||||
|
const path = proxy['http-path'];
|
||||||
|
if (Array.isArray(path)) {
|
||||||
|
transport.path = `${path[0]}`;
|
||||||
|
} else if (path !== '') transport.path = `${path}`;
|
||||||
|
}
|
||||||
|
if (parsedProxy.tls.insecure)
|
||||||
|
parsedProxy.tls.server_name = transport.host[0];
|
||||||
|
if (transport.host.length === 1) transport.host = transport.host[0];
|
||||||
|
for (const key of Object.keys(transport.headers)) {
|
||||||
|
const value = transport.headers[key];
|
||||||
|
if (value.length === 1) transport.headers[key] = value[0];
|
||||||
|
}
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const h2Parser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'http' };
|
||||||
|
if (proxy['h2-opts']) {
|
||||||
|
let { host = '', path = '' } = proxy['h2-opts'];
|
||||||
|
if (path !== '') transport.path = `${path}`;
|
||||||
|
if (host !== '') {
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['h2-host'] && proxy['h2-host'] !== '') {
|
||||||
|
let host = proxy['h2-host'];
|
||||||
|
if (!Array.isArray(host))
|
||||||
|
host = `${host}`.split(',').map((i) => i.trim());
|
||||||
|
if (host.length > 0) transport.host = host;
|
||||||
|
}
|
||||||
|
if (proxy['h2-path'] && proxy['h2-path'] !== '')
|
||||||
|
transport.path = `${proxy['h2-path']}`;
|
||||||
|
parsedProxy.tls.enabled = true;
|
||||||
|
if (parsedProxy.tls.insecure)
|
||||||
|
parsedProxy.tls.server_name = transport.host[0];
|
||||||
|
if (transport.host.length === 1) transport.host = transport.host[0];
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const grpcParser = (proxy, parsedProxy) => {
|
||||||
|
const transport = { type: 'grpc' };
|
||||||
|
if (proxy['grpc-opts']) {
|
||||||
|
const serviceName = proxy['grpc-opts']['grpc-service-name'];
|
||||||
|
if (serviceName != null && serviceName !== '')
|
||||||
|
transport.service_name = `${serviceName}`;
|
||||||
|
}
|
||||||
|
parsedProxy.transport = transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tlsParser = (proxy, parsedProxy) => {
|
||||||
|
if (proxy.tls) parsedProxy.tls.enabled = true;
|
||||||
|
if (proxy.servername && proxy.servername !== '')
|
||||||
|
parsedProxy.tls.server_name = proxy.servername;
|
||||||
|
if (proxy.peer && proxy.peer !== '')
|
||||||
|
parsedProxy.tls.server_name = proxy.peer;
|
||||||
|
if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
|
||||||
|
if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
|
||||||
|
if (proxy.insecure) parsedProxy.tls.insecure = true;
|
||||||
|
if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
|
||||||
|
if (typeof proxy.alpn === 'string') {
|
||||||
|
parsedProxy.tls.alpn = [proxy.alpn];
|
||||||
|
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
|
||||||
|
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
|
||||||
|
if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
|
||||||
|
if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
|
||||||
|
if (proxy['reality-opts']) {
|
||||||
|
parsedProxy.tls.reality = { enabled: true };
|
||||||
|
if (proxy['reality-opts']['public-key'])
|
||||||
|
parsedProxy.tls.reality.public_key =
|
||||||
|
proxy['reality-opts']['public-key'];
|
||||||
|
if (proxy['reality-opts']['short-id'])
|
||||||
|
parsedProxy.tls.reality.short_id =
|
||||||
|
proxy['reality-opts']['short-id'];
|
||||||
|
parsedProxy.tls.utls = { enabled: true };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&
|
||||||
|
proxy['client-fingerprint'] &&
|
||||||
|
proxy['client-fingerprint'] !== ''
|
||||||
|
)
|
||||||
|
parsedProxy.tls.utls = {
|
||||||
|
enabled: true,
|
||||||
|
fingerprint: proxy['client-fingerprint'],
|
||||||
|
};
|
||||||
|
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sshParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'ssh',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.username) parsedProxy.user = proxy.username;
|
||||||
|
if (proxy.password) parsedProxy.password = proxy.password;
|
||||||
|
// https://wiki.metacubex.one/config/proxies/ssh
|
||||||
|
// https://sing-box.sagernet.org/zh/configuration/outbound/ssh
|
||||||
|
if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
|
||||||
|
if (proxy['private-key'])
|
||||||
|
parsedProxy.private_key_path = proxy['private-key'];
|
||||||
|
if (proxy['private-key-passphrase'])
|
||||||
|
parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
|
||||||
|
if (proxy['server-fingerprint']) {
|
||||||
|
parsedProxy.host_key = [proxy['server-fingerprint']];
|
||||||
|
// https://manual.nssurge.com/policy/ssh.html
|
||||||
|
// Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.)
|
||||||
|
// TODO: ?
|
||||||
|
parsedProxy.host_key_algorithms = [
|
||||||
|
proxy['server-fingerprint'].split(' ')[0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];
|
||||||
|
if (proxy['host-key-algorithms'])
|
||||||
|
parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'http',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.username) parsedProxy.username = proxy.username;
|
||||||
|
if (proxy.password) parsedProxy.password = proxy.password;
|
||||||
|
if (proxy.headers) {
|
||||||
|
parsedProxy.headers = {};
|
||||||
|
for (const k of Object.keys(proxy.headers)) {
|
||||||
|
parsedProxy.headers[k] = `${proxy.headers[k]}`;
|
||||||
|
}
|
||||||
|
if (Object.keys(parsedProxy.headers).length === 0)
|
||||||
|
delete parsedProxy.headers;
|
||||||
|
}
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const socks5Parser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'socks',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
version: '5',
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.username) parsedProxy.username = proxy.username;
|
||||||
|
if (proxy.password) parsedProxy.password = proxy.password;
|
||||||
|
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shadowTLSParser = (proxy = {}) => {
|
||||||
|
const ssPart = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'shadowsocks',
|
||||||
|
method: proxy.cipher,
|
||||||
|
password: proxy.password,
|
||||||
|
detour: `${proxy.name}_shadowtls`,
|
||||||
|
};
|
||||||
|
const stPart = {
|
||||||
|
tag: `${proxy.name}_shadowtls`,
|
||||||
|
type: 'shadowtls',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
version: proxy['plugin-opts'].version,
|
||||||
|
password: proxy['plugin-opts'].password,
|
||||||
|
tls: {
|
||||||
|
enabled: true,
|
||||||
|
server_name: proxy['plugin-opts'].host,
|
||||||
|
utls: {
|
||||||
|
enabled: true,
|
||||||
|
fingerprint: proxy['client-fingerprint'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (stPart.server_port < 0 || stPart.server_port > 65535)
|
||||||
|
throw '端口值非法';
|
||||||
|
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
|
||||||
|
tfoParser(proxy, stPart);
|
||||||
|
detourParser(proxy, stPart);
|
||||||
|
smuxParser(proxy.smux, ssPart);
|
||||||
|
return { type: 'ss-with-st', ssPart, stPart };
|
||||||
|
};
|
||||||
|
const ssParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'shadowsocks',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
method: proxy.cipher,
|
||||||
|
password: proxy.password,
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.uot) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
if (proxy.plugin) {
|
||||||
|
const optArr = [];
|
||||||
|
if (proxy.plugin === 'obfs') {
|
||||||
|
parsedProxy.plugin = 'obfs-local';
|
||||||
|
parsedProxy.plugin_opts = '';
|
||||||
|
if (proxy['obfs-host'])
|
||||||
|
proxy['plugin-opts'].host = proxy['obfs-host'];
|
||||||
|
Object.keys(proxy['plugin-opts']).forEach((k) => {
|
||||||
|
switch (k) {
|
||||||
|
case 'mode':
|
||||||
|
optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
|
||||||
|
break;
|
||||||
|
case 'host':
|
||||||
|
optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (proxy.plugin === 'v2ray-plugin') {
|
||||||
|
parsedProxy.plugin = 'v2ray-plugin';
|
||||||
|
if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
|
||||||
|
if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
|
||||||
|
Object.keys(proxy['plugin-opts']).forEach((k) => {
|
||||||
|
switch (k) {
|
||||||
|
case 'tls':
|
||||||
|
if (proxy['plugin-opts'].tls) optArr.push('tls');
|
||||||
|
break;
|
||||||
|
case 'host':
|
||||||
|
optArr.push(`host=${proxy['plugin-opts'].host}`);
|
||||||
|
break;
|
||||||
|
case 'path':
|
||||||
|
optArr.push(`path=${proxy['plugin-opts'].path}`);
|
||||||
|
break;
|
||||||
|
case 'headers':
|
||||||
|
optArr.push(
|
||||||
|
`headers=${JSON.stringify(
|
||||||
|
proxy['plugin-opts'].headers,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'mux':
|
||||||
|
if (proxy['plugin-opts'].mux)
|
||||||
|
parsedProxy.multiplex = { enabled: true };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
parsedProxy.plugin_opts = optArr.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const ssrParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'shadowsocksr',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
method: proxy.cipher,
|
||||||
|
password: proxy.password,
|
||||||
|
obfs: proxy.obfs,
|
||||||
|
protocol: proxy.protocol,
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
|
||||||
|
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
|
||||||
|
parsedProxy.protocol_param = proxy['protocol-param'];
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const vmessParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'vmess',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
security: proxy.cipher,
|
||||||
|
alter_id: parseInt(`${proxy.alterId}`, 10),
|
||||||
|
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'auto',
|
||||||
|
'none',
|
||||||
|
'zero',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'aes-128-ctr',
|
||||||
|
].indexOf(parsedProxy.security) === -1
|
||||||
|
)
|
||||||
|
parsedProxy.security = 'auto';
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const vlessParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'vless',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
tls: { enabled: false, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
||||||
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const trojanParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'trojan',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
|
||||||
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const hysteriaParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'hysteria',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
disable_mtu_discovery: false,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
|
||||||
|
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
|
||||||
|
if (reg.test(`${proxy.up}`)) {
|
||||||
|
parsedProxy.up = `${proxy.up}`;
|
||||||
|
} else {
|
||||||
|
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||||
|
}
|
||||||
|
if (reg.test(`${proxy.down}`)) {
|
||||||
|
parsedProxy.down = `${proxy.down}`;
|
||||||
|
} else {
|
||||||
|
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||||
|
}
|
||||||
|
if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
|
||||||
|
if (proxy.recv_window_conn)
|
||||||
|
parsedProxy.recv_window_conn = proxy.recv_window_conn;
|
||||||
|
if (proxy['recv-window-conn'])
|
||||||
|
parsedProxy.recv_window_conn = proxy['recv-window-conn'];
|
||||||
|
if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
|
||||||
|
if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
|
||||||
|
if (proxy.disable_mtu_discovery) {
|
||||||
|
if (typeof proxy.disable_mtu_discovery === 'boolean') {
|
||||||
|
parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
|
||||||
|
} else {
|
||||||
|
if (proxy.disable_mtu_discovery === 1)
|
||||||
|
parsedProxy.disable_mtu_discovery = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'hysteria2',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
obfs: {},
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (includeUnsupportedProxy) {
|
||||||
|
if (proxy['hop-interval'])
|
||||||
|
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
|
||||||
|
? `${proxy['hop-interval']}s`
|
||||||
|
: proxy['hop-interval'];
|
||||||
|
if (proxy['ports'])
|
||||||
|
parsedProxy.server_ports = proxy['ports']
|
||||||
|
.split(/\s*,\s*/)
|
||||||
|
.map((p) => p.replace(/\s*-\s*/g, ':'));
|
||||||
|
}
|
||||||
|
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||||
|
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
|
||||||
|
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
|
||||||
|
if (proxy['obfs-password'])
|
||||||
|
parsedProxy.obfs.password = proxy['obfs-password'];
|
||||||
|
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const tuic5Parser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'tuic',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
uuid: proxy.uuid,
|
||||||
|
password: proxy.password,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (
|
||||||
|
proxy['congestion-controller'] &&
|
||||||
|
proxy['congestion-controller'] !== 'cubic'
|
||||||
|
)
|
||||||
|
parsedProxy.congestion_control = proxy['congestion-controller'];
|
||||||
|
if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
|
||||||
|
parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
|
||||||
|
if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
|
||||||
|
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
|
||||||
|
if (proxy['heartbeat-interval'])
|
||||||
|
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
const anytlsParser = (proxy = {}) => {
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'anytls',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
password: proxy.password,
|
||||||
|
tls: { enabled: true, server_name: proxy.server, insecure: false },
|
||||||
|
};
|
||||||
|
if (/^\d+$/.test(proxy['idle-session-check-interval']))
|
||||||
|
parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
|
||||||
|
if (/^\d+$/.test(proxy['idle-session-timeout']))
|
||||||
|
parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
tlsParser(proxy, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wireguardParser = (proxy = {}) => {
|
||||||
|
const local_address = ['ip', 'ipv6']
|
||||||
|
.map((i) => proxy[i])
|
||||||
|
.map((i) => {
|
||||||
|
if (isIPv4(i)) return `${i}/32`;
|
||||||
|
if (isIPv6(i)) return `${i}/128`;
|
||||||
|
})
|
||||||
|
.filter((i) => i);
|
||||||
|
const parsedProxy = {
|
||||||
|
tag: proxy.name,
|
||||||
|
type: 'wireguard',
|
||||||
|
server: proxy.server,
|
||||||
|
server_port: parseInt(`${proxy.port}`, 10),
|
||||||
|
local_address,
|
||||||
|
private_key: proxy['private-key'],
|
||||||
|
peer_public_key: proxy['public-key'],
|
||||||
|
pre_shared_key: proxy['pre-shared-key'],
|
||||||
|
reserved: [],
|
||||||
|
};
|
||||||
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
|
throw 'invalid port';
|
||||||
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
|
if (typeof proxy.reserved === 'string') {
|
||||||
|
parsedProxy.reserved = proxy.reserved;
|
||||||
|
} else if (Array.isArray(proxy.reserved)) {
|
||||||
|
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
|
||||||
|
} else {
|
||||||
|
delete parsedProxy.reserved;
|
||||||
|
}
|
||||||
|
if (proxy.peers && proxy.peers.length > 0) {
|
||||||
|
parsedProxy.peers = [];
|
||||||
|
for (const p of proxy.peers) {
|
||||||
|
const peer = {
|
||||||
|
server: p.server,
|
||||||
|
server_port: parseInt(`${p.port}`, 10),
|
||||||
|
public_key: p['public-key'],
|
||||||
|
allowed_ips: p['allowed-ips'] || p.allowed_ips,
|
||||||
|
reserved: [],
|
||||||
|
};
|
||||||
|
if (typeof p.reserved === 'string') {
|
||||||
|
peer.reserved.push(p.reserved);
|
||||||
|
} else if (Array.isArray(p.reserved)) {
|
||||||
|
for (const r of p.reserved) peer.reserved.push(r);
|
||||||
|
} else {
|
||||||
|
delete peer.reserved;
|
||||||
|
}
|
||||||
|
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
|
||||||
|
parsedProxy.peers.push(peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
networkParser(proxy, parsedProxy);
|
||||||
|
tfoParser(proxy, parsedProxy);
|
||||||
|
detourParser(proxy, parsedProxy);
|
||||||
|
smuxParser(proxy.smux, parsedProxy);
|
||||||
|
return parsedProxy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function singbox_Producer() {
|
||||||
|
const type = 'ALL';
|
||||||
|
const produce = (proxies, type, opts = {}) => {
|
||||||
|
const list = [];
|
||||||
|
ClashMeta_Producer()
|
||||||
|
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
|
||||||
|
.map((proxy) => {
|
||||||
|
try {
|
||||||
|
switch (proxy.type) {
|
||||||
|
case 'ssh':
|
||||||
|
list.push(sshParser(proxy));
|
||||||
|
break;
|
||||||
|
case 'http':
|
||||||
|
list.push(httpParser(proxy));
|
||||||
|
break;
|
||||||
|
case 'socks5':
|
||||||
|
if (proxy.tls) {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with tls`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
list.push(socks5Parser(proxy));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ss':
|
||||||
|
// if (!proxy.cipher) {
|
||||||
|
// proxy.cipher = 'none';
|
||||||
|
// }
|
||||||
|
// if (
|
||||||
|
// ![
|
||||||
|
// '2022-blake3-aes-128-gcm',
|
||||||
|
// '2022-blake3-aes-256-gcm',
|
||||||
|
// '2022-blake3-chacha20-poly1305',
|
||||||
|
// 'aes-128-cfb',
|
||||||
|
// 'aes-128-ctr',
|
||||||
|
// 'aes-128-gcm',
|
||||||
|
// 'aes-192-cfb',
|
||||||
|
// 'aes-192-ctr',
|
||||||
|
// 'aes-192-gcm',
|
||||||
|
// 'aes-256-cfb',
|
||||||
|
// 'aes-256-ctr',
|
||||||
|
// 'aes-256-gcm',
|
||||||
|
// 'chacha20-ietf',
|
||||||
|
// 'chacha20-ietf-poly1305',
|
||||||
|
// 'none',
|
||||||
|
// 'rc4-md5',
|
||||||
|
// 'xchacha20',
|
||||||
|
// 'xchacha20-ietf-poly1305',
|
||||||
|
// ].includes(proxy.cipher)
|
||||||
|
// ) {
|
||||||
|
// throw new Error(
|
||||||
|
// `cipher ${proxy.cipher} is not supported`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
if (proxy.plugin === 'shadow-tls') {
|
||||||
|
const { ssPart, stPart } =
|
||||||
|
shadowTLSParser(proxy);
|
||||||
|
list.push(ssPart);
|
||||||
|
list.push(stPart);
|
||||||
|
} else {
|
||||||
|
list.push(ssParser(proxy));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ssr':
|
||||||
|
if (opts['include-unsupported-proxy']) {
|
||||||
|
list.push(ssrParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'vmess':
|
||||||
|
if (
|
||||||
|
!proxy.network ||
|
||||||
|
['ws', 'grpc', 'h2', 'http'].includes(
|
||||||
|
proxy.network,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
list.push(vmessParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'vless':
|
||||||
|
if (
|
||||||
|
!proxy.flow ||
|
||||||
|
['xtls-rprx-vision'].includes(proxy.flow)
|
||||||
|
) {
|
||||||
|
list.push(vlessParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'trojan':
|
||||||
|
if (!proxy.flow) {
|
||||||
|
list.push(trojanParser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'hysteria':
|
||||||
|
list.push(hysteriaParser(proxy));
|
||||||
|
break;
|
||||||
|
case 'hysteria2':
|
||||||
|
list.push(
|
||||||
|
hysteria2Parser(
|
||||||
|
proxy,
|
||||||
|
opts['include-unsupported-proxy'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'tuic':
|
||||||
|
if (!proxy.token || proxy.token.length === 0) {
|
||||||
|
list.push(tuic5Parser(proxy));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: TUIC v4`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'wireguard':
|
||||||
|
list.push(wireguardParser(proxy));
|
||||||
|
break;
|
||||||
|
case 'anytls':
|
||||||
|
list.push(anytlsParser(proxy));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Platform sing-box does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e);
|
||||||
|
$.error(e.message ?? e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return type === 'internal'
|
||||||
|
? list
|
||||||
|
: JSON.stringify({ outbounds: list }, null, 2);
|
||||||
|
};
|
||||||
|
return { type, produce };
|
||||||
|
}
|
||||||
@@ -2,223 +2,306 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
|
|||||||
|
|
||||||
export default function Stash_Producer() {
|
export default function Stash_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) => {
|
const produce = (proxies, type, opts = {}) => {
|
||||||
return (
|
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
|
||||||
'proxies:\n' +
|
const list = proxies
|
||||||
proxies
|
.filter((proxy) => {
|
||||||
.filter((proxy) => {
|
if (
|
||||||
if (
|
![
|
||||||
|
'ss',
|
||||||
|
'ssr',
|
||||||
|
'vmess',
|
||||||
|
'socks5',
|
||||||
|
'http',
|
||||||
|
'snell',
|
||||||
|
'trojan',
|
||||||
|
'tuic',
|
||||||
|
'vless',
|
||||||
|
'wireguard',
|
||||||
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
|
'ssh',
|
||||||
|
'juicity',
|
||||||
|
].includes(proxy.type) ||
|
||||||
|
(proxy.type === 'ss' &&
|
||||||
![
|
![
|
||||||
'ss',
|
'aes-128-gcm',
|
||||||
'ssr',
|
'aes-192-gcm',
|
||||||
'vmess',
|
'aes-256-gcm',
|
||||||
'socks5',
|
'aes-128-cfb',
|
||||||
'http',
|
'aes-192-cfb',
|
||||||
'snell',
|
'aes-256-cfb',
|
||||||
'trojan',
|
'aes-128-ctr',
|
||||||
'tuic',
|
'aes-192-ctr',
|
||||||
'vless',
|
'aes-256-ctr',
|
||||||
'wireguard',
|
'rc4-md5',
|
||||||
'hysteria',
|
'chacha20-ietf',
|
||||||
'hysteria2',
|
'xchacha20',
|
||||||
].includes(proxy.type) ||
|
'chacha20-ietf-poly1305',
|
||||||
(proxy.type === 'snell' &&
|
'xchacha20-ietf-poly1305',
|
||||||
String(proxy.version) === '4') ||
|
...(opts['include-unsupported-proxy']
|
||||||
(proxy.type === 'vless' && proxy['reality-opts'])
|
? [
|
||||||
) {
|
'2022-blake3-aes-128-gcm',
|
||||||
return false;
|
'2022-blake3-aes-256-gcm',
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
].includes(proxy.cipher)) ||
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
return true;
|
if (isPresent(proxy, 'sni')) {
|
||||||
})
|
proxy.servername = proxy.sni;
|
||||||
.map((proxy) => {
|
delete proxy.sni;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||||
|
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||||
if (
|
if (
|
||||||
['vmess', 'vless'].includes(proxy.type) &&
|
isPresent(proxy, 'cipher') &&
|
||||||
proxy.network === 'http'
|
![
|
||||||
|
'auto',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'none',
|
||||||
|
].includes(proxy.cipher)
|
||||||
) {
|
) {
|
||||||
let httpPath = proxy['http-opts']?.path;
|
proxy.cipher = 'auto';
|
||||||
if (
|
}
|
||||||
isPresent(proxy, 'http-opts.path') &&
|
} else if (proxy.type === 'tuic') {
|
||||||
!Array.isArray(httpPath)
|
if (isPresent(proxy, 'alpn')) {
|
||||||
) {
|
proxy.alpn = Array.isArray(proxy.alpn)
|
||||||
proxy['http-opts'].path = [httpPath];
|
? proxy.alpn
|
||||||
}
|
: [proxy.alpn];
|
||||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
} else {
|
||||||
if (
|
proxy.alpn = ['h3'];
|
||||||
isPresent(proxy, 'http-opts.headers.Host') &&
|
|
||||||
!Array.isArray(httpHost)
|
|
||||||
) {
|
|
||||||
proxy['http-opts'].headers.Host = [httpHost];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
|
isPresent(proxy, 'tfo') &&
|
||||||
proxy.type,
|
!isPresent(proxy, 'fast-open')
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
delete proxy.tls;
|
proxy['fast-open'] = proxy.tfo;
|
||||||
|
delete proxy.tfo;
|
||||||
}
|
}
|
||||||
if (proxy['tls-fingerprint']) {
|
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||||
proxy.fingerprint = proxy['tls-fingerprint'];
|
|
||||||
}
|
|
||||||
delete proxy['tls-fingerprint'];
|
|
||||||
|
|
||||||
if (proxy['test-url']) {
|
|
||||||
proxy['benchmark-url'] = proxy['test-url'];
|
|
||||||
delete proxy['test-url'];
|
|
||||||
}
|
|
||||||
|
|
||||||
delete proxy.subName;
|
|
||||||
delete proxy.collectionName;
|
|
||||||
if (
|
if (
|
||||||
['grpc'].includes(proxy.network) &&
|
(!proxy.token || proxy.token.length === 0) &&
|
||||||
proxy[`${proxy.network}-opts`]
|
!isPresent(proxy, 'version')
|
||||||
) {
|
) {
|
||||||
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
proxy.version = 5;
|
||||||
}
|
}
|
||||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
} else if (proxy.type === 'hysteria') {
|
||||||
})
|
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||||
.join('')
|
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 === 'snell' && proxy.version < 3) {
|
||||||
|
delete proxy.udp;
|
||||||
|
} 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 (
|
||||||
|
['vmess', 'vless'].includes(proxy.type) &&
|
||||||
|
proxy.network === 'h2'
|
||||||
|
) {
|
||||||
|
let path = proxy['h2-opts']?.path;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.path') &&
|
||||||
|
Array.isArray(path)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].path = path[0];
|
||||||
|
}
|
||||||
|
let host = proxy['h2-opts']?.headers?.host;
|
||||||
|
if (
|
||||||
|
isPresent(proxy, 'h2-opts.headers.Host') &&
|
||||||
|
!Array.isArray(host)
|
||||||
|
) {
|
||||||
|
proxy['h2-opts'].headers.host = [host];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxy['plugin-opts']?.tls) {
|
||||||
|
if (isPresent(proxy, 'skip-cert-verify')) {
|
||||||
|
proxy['plugin-opts']['skip-cert-verify'] =
|
||||||
|
proxy['skip-cert-verify'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
'trojan',
|
||||||
|
'tuic',
|
||||||
|
'hysteria',
|
||||||
|
'hysteria2',
|
||||||
|
'juicity',
|
||||||
|
'anytls',
|
||||||
|
].includes(proxy.type)
|
||||||
|
) {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
if (proxy['tls-fingerprint']) {
|
||||||
|
proxy['server-cert-fingerprint'] = proxy['tls-fingerprint'];
|
||||||
|
}
|
||||||
|
delete proxy['tls-fingerprint'];
|
||||||
|
|
||||||
|
if (proxy['underlying-proxy']) {
|
||||||
|
proxy['dialer-proxy'] = proxy['underlying-proxy'];
|
||||||
|
}
|
||||||
|
delete proxy['underlying-proxy'];
|
||||||
|
|
||||||
|
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy['test-url']) {
|
||||||
|
proxy['benchmark-url'] = proxy['test-url'];
|
||||||
|
delete proxy['test-url'];
|
||||||
|
}
|
||||||
|
if (proxy['test-timeout']) {
|
||||||
|
proxy['benchmark-timeout'] = proxy['test-timeout'];
|
||||||
|
delete proxy['test-timeout'];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
delete proxy.id;
|
||||||
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
|
if (type !== 'internal') {
|
||||||
|
for (const key in proxy) {
|
||||||
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
|
delete proxy[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['grpc'].includes(proxy.network) &&
|
||||||
|
proxy[`${proxy.network}-opts`]
|
||||||
|
) {
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
|
||||||
|
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
|
||||||
|
}
|
||||||
|
return proxy;
|
||||||
|
});
|
||||||
|
return type === 'internal'
|
||||||
|
? list
|
||||||
|
: 'proxies:\n' +
|
||||||
|
list
|
||||||
|
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||||
|
.join('');
|
||||||
};
|
};
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|||||||
226
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
226
backend/src/core/proxy-utils/producers/surfboard.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { Result, isPresent } from './utils';
|
||||||
|
import { isNotBlank } from '@/utils';
|
||||||
|
// import $ from '@/core/app';
|
||||||
|
|
||||||
|
const targetPlatform = 'Surfboard';
|
||||||
|
|
||||||
|
export default function Surfboard_Producer() {
|
||||||
|
const produce = (proxy) => {
|
||||||
|
proxy.name = proxy.name.replace(/=|,/g, '');
|
||||||
|
switch (proxy.type) {
|
||||||
|
case 'ss':
|
||||||
|
return shadowsocks(proxy);
|
||||||
|
case 'trojan':
|
||||||
|
return trojan(proxy);
|
||||||
|
case 'vmess':
|
||||||
|
return vmess(proxy);
|
||||||
|
case 'http':
|
||||||
|
return http(proxy);
|
||||||
|
case 'socks5':
|
||||||
|
return socks5(proxy);
|
||||||
|
case 'wireguard-surge':
|
||||||
|
return wireguard(proxy);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return { produce };
|
||||||
|
}
|
||||||
|
|
||||||
|
function shadowsocks(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'aes-128-gcm',
|
||||||
|
'aes-192-gcm',
|
||||||
|
'aes-256-gcm',
|
||||||
|
'chacha20-ietf-poly1305',
|
||||||
|
'xchacha20-ietf-poly1305',
|
||||||
|
'rc4',
|
||||||
|
'rc4-md5',
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-192-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'aes-128-ctr',
|
||||||
|
'aes-192-ctr',
|
||||||
|
'aes-256-ctr',
|
||||||
|
'bf-cfb',
|
||||||
|
'camellia-128-cfb',
|
||||||
|
'camellia-192-cfb',
|
||||||
|
'camellia-256-cfb',
|
||||||
|
'salsa20',
|
||||||
|
'chacha20',
|
||||||
|
'chacha20-ietf',
|
||||||
|
].includes(proxy.cipher)
|
||||||
|
) {
|
||||||
|
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||||
|
}
|
||||||
|
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||||
|
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// obfs
|
||||||
|
if (isPresent(proxy, 'plugin')) {
|
||||||
|
if (proxy.plugin === 'obfs') {
|
||||||
|
result.append(`,obfs=${proxy['plugin-opts'].mode}`);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,obfs-host=${proxy['plugin-opts'].host}`,
|
||||||
|
'plugin-opts.host',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||||
|
'plugin-opts.path',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function trojan(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// transport
|
||||||
|
handleTransport(result, proxy);
|
||||||
|
|
||||||
|
// tls
|
||||||
|
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vmess(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||||
|
|
||||||
|
// transport
|
||||||
|
handleTransport(result, proxy);
|
||||||
|
|
||||||
|
// AEAD
|
||||||
|
if (isPresent(proxy, 'aead')) {
|
||||||
|
result.append(`,vmess-aead=${proxy.aead}`);
|
||||||
|
} else {
|
||||||
|
result.append(`,vmess-aead=${proxy.alterId === 0}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tls
|
||||||
|
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function http(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
const type = proxy.tls ? 'https' : 'http';
|
||||||
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
|
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function socks5(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
const type = proxy.tls ? 'socks5-tls' : 'socks5';
|
||||||
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
|
result.appendIfPresent(`,${proxy.password}`, 'password');
|
||||||
|
|
||||||
|
// tls verification
|
||||||
|
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||||
|
'skip-cert-verify',
|
||||||
|
);
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTransport(result, proxy) {
|
||||||
|
if (isPresent(proxy, 'network')) {
|
||||||
|
if (proxy.network === 'ws') {
|
||||||
|
result.append(`,ws=true`);
|
||||||
|
if (isPresent(proxy, 'ws-opts')) {
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,ws-path=${proxy['ws-opts'].path}`,
|
||||||
|
'ws-opts.path',
|
||||||
|
);
|
||||||
|
if (isPresent(proxy, 'ws-opts.headers')) {
|
||||||
|
const headers = proxy['ws-opts'].headers;
|
||||||
|
const value = Object.keys(headers)
|
||||||
|
.map((k) => {
|
||||||
|
let v = headers[k];
|
||||||
|
if (['Host'].includes(k)) {
|
||||||
|
v = `"${v}"`;
|
||||||
|
}
|
||||||
|
return `${k}:${v}`;
|
||||||
|
})
|
||||||
|
.join('|');
|
||||||
|
if (isNotBlank(value)) {
|
||||||
|
result.append(`,ws-headers=${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Result, isPresent } from './utils';
|
import { Result, isPresent } from './utils';
|
||||||
import { isNotBlank } from '@/utils';
|
import { isNotBlank, getIfNotBlank } from '@/utils';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
|
||||||
const targetPlatform = 'Surge';
|
const targetPlatform = 'Surge';
|
||||||
@@ -13,16 +13,22 @@ const ipVersions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Surge_Producer() {
|
export default function Surge_Producer() {
|
||||||
const produce = (proxy) => {
|
const produce = (proxy, type, opts = {}) => {
|
||||||
|
proxy.name = proxy.name.replace(/=|,/g, '');
|
||||||
|
if (proxy.ports) {
|
||||||
|
proxy.ports = String(proxy.ports);
|
||||||
|
}
|
||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'ss':
|
case 'ss':
|
||||||
return shadowsocks(proxy);
|
return shadowsocks(proxy, opts['include-unsupported-proxy']);
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
return trojan(proxy);
|
return trojan(proxy);
|
||||||
case 'vmess':
|
case 'vmess':
|
||||||
return vmess(proxy);
|
return vmess(proxy, opts['include-unsupported-proxy']);
|
||||||
case 'http':
|
case 'http':
|
||||||
return http(proxy);
|
return http(proxy);
|
||||||
|
case 'direct':
|
||||||
|
return direct(proxy);
|
||||||
case 'socks5':
|
case 'socks5':
|
||||||
return socks5(proxy);
|
return socks5(proxy);
|
||||||
case 'snell':
|
case 'snell':
|
||||||
@@ -30,9 +36,15 @@ export default function Surge_Producer() {
|
|||||||
case 'tuic':
|
case 'tuic':
|
||||||
return tuic(proxy);
|
return tuic(proxy);
|
||||||
case 'wireguard-surge':
|
case 'wireguard-surge':
|
||||||
return wireguard(proxy);
|
return wireguard_surge(proxy);
|
||||||
case 'hysteria2':
|
case 'hysteria2':
|
||||||
return hysteria2(proxy);
|
return hysteria2(proxy);
|
||||||
|
case 'ssh':
|
||||||
|
return ssh(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {
|
||||||
|
return wireguard(proxy);
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||||
@@ -44,13 +56,48 @@ export default function Surge_Producer() {
|
|||||||
function shadowsocks(proxy) {
|
function shadowsocks(proxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
|
if (!proxy.cipher) {
|
||||||
|
proxy.cipher = 'none';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'aes-128-gcm',
|
||||||
|
'aes-192-gcm',
|
||||||
|
'aes-256-gcm',
|
||||||
|
'chacha20-ietf-poly1305',
|
||||||
|
'xchacha20-ietf-poly1305',
|
||||||
|
'rc4',
|
||||||
|
'rc4-md5',
|
||||||
|
'aes-128-cfb',
|
||||||
|
'aes-192-cfb',
|
||||||
|
'aes-256-cfb',
|
||||||
|
'aes-128-ctr',
|
||||||
|
'aes-192-ctr',
|
||||||
|
'aes-256-ctr',
|
||||||
|
'bf-cfb',
|
||||||
|
'camellia-128-cfb',
|
||||||
|
'camellia-192-cfb',
|
||||||
|
'camellia-256-cfb',
|
||||||
|
'cast5-cfb',
|
||||||
|
'des-cfb',
|
||||||
|
'idea-cfb',
|
||||||
|
'rc2-cfb',
|
||||||
|
'seed-cfb',
|
||||||
|
'salsa20',
|
||||||
|
'chacha20',
|
||||||
|
'chacha20-ietf',
|
||||||
|
'none',
|
||||||
|
'2022-blake3-aes-128-gcm',
|
||||||
|
'2022-blake3-aes-256-gcm',
|
||||||
|
].includes(proxy.cipher)
|
||||||
|
) {
|
||||||
|
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||||
|
}
|
||||||
result.append(`,encrypt-method=${proxy.cipher}`);
|
result.append(`,encrypt-method=${proxy.cipher}`);
|
||||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
@@ -69,7 +116,7 @@ function shadowsocks(proxy) {
|
|||||||
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||||
'plugin-opts.path',
|
'plugin-opts.path',
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!['shadow-tls'].includes(proxy.plugin)) {
|
||||||
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
throw new Error(`plugin ${proxy.plugin} is not supported`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +129,21 @@ function shadowsocks(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -95,6 +157,31 @@ function shadowsocks(proxy) {
|
|||||||
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
|
||||||
'shadow-tls-sni',
|
'shadow-tls-sni',
|
||||||
);
|
);
|
||||||
|
// udp-port
|
||||||
|
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
|
||||||
|
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
|
||||||
|
const password = proxy['plugin-opts'].password;
|
||||||
|
const host = proxy['plugin-opts'].host;
|
||||||
|
const version = proxy['plugin-opts'].version;
|
||||||
|
if (password) {
|
||||||
|
result.append(`,shadow-tls-password=${password}`);
|
||||||
|
if (host) {
|
||||||
|
result.append(`,shadow-tls-sni=${host}`);
|
||||||
|
}
|
||||||
|
if (version) {
|
||||||
|
if (version < 2) {
|
||||||
|
throw new Error(
|
||||||
|
`shadow-tls version ${version} is not supported`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result.append(`,shadow-tls-version=${version}`);
|
||||||
|
}
|
||||||
|
// udp-port
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,udp-port=${proxy['udp-port']}`,
|
||||||
|
'udp-port',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// block-quic
|
// block-quic
|
||||||
@@ -112,12 +199,10 @@ function shadowsocks(proxy) {
|
|||||||
function trojan(proxy) {
|
function trojan(proxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
@@ -151,6 +236,21 @@ function trojan(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -178,15 +278,13 @@ function trojan(proxy) {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function vmess(proxy) {
|
function vmess(proxy, includeUnsupportedProxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
@@ -194,7 +292,7 @@ function vmess(proxy) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// transport
|
// transport
|
||||||
handleTransport(result, proxy);
|
handleTransport(result, proxy, includeUnsupportedProxy);
|
||||||
|
|
||||||
// AEAD
|
// AEAD
|
||||||
if (isPresent(proxy, 'aead')) {
|
if (isPresent(proxy, 'aead')) {
|
||||||
@@ -227,6 +325,21 @@ function vmess(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -254,17 +367,83 @@ function vmess(proxy) {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ssh(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
|
||||||
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
|
// 所有的类似的字段都有双引号的问题 暂不处理
|
||||||
|
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||||
|
|
||||||
|
// https://manual.nssurge.com/policy/ssh.html
|
||||||
|
// 需配合 Keystore
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,private-key=${proxy['keystore-private-key']}`,
|
||||||
|
'keystore-private-key',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,idle-timeout=${proxy['idle-timeout']}`,
|
||||||
|
'idle-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,server-fingerprint="${proxy['server-fingerprint']}"`,
|
||||||
|
'server-fingerprint',
|
||||||
|
);
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
|
'no-error-alert',
|
||||||
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
// test-url
|
||||||
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 http(proxy) {
|
function http(proxy) {
|
||||||
|
if (proxy.headers && Object.keys(proxy.headers).length > 0) {
|
||||||
|
throw new Error(`headers is unsupported`);
|
||||||
|
}
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
const type = proxy.tls ? 'https' : 'http';
|
const type = proxy.tls ? 'https' : 'http';
|
||||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
@@ -292,6 +471,21 @@ function http(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -318,18 +512,64 @@ function http(proxy) {
|
|||||||
|
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
function direct(proxy) {
|
||||||
|
const result = new Result(proxy);
|
||||||
|
const type = 'direct';
|
||||||
|
result.append(`${proxy.name}=${type}`);
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
|
'no-error-alert',
|
||||||
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
|
// udp
|
||||||
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
|
// test-url
|
||||||
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 socks5(proxy) {
|
function socks5(proxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
const type = proxy.tls ? 'socks5-tls' : 'socks5';
|
const type = proxy.tls ? 'socks5-tls' : 'socks5';
|
||||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
result.appendIfPresent(`,${proxy.username}`, 'username');
|
result.appendIfPresent(`,${proxy.username}`, 'username');
|
||||||
result.appendIfPresent(`,${proxy.password}`, 'password');
|
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
@@ -359,6 +599,21 @@ function socks5(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -392,10 +647,8 @@ function snell(proxy) {
|
|||||||
result.appendIfPresent(`,version=${proxy.version}`, 'version');
|
result.appendIfPresent(`,version=${proxy.version}`, 'version');
|
||||||
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
|
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
@@ -416,11 +669,29 @@ function snell(proxy) {
|
|||||||
'obfs-opts.path',
|
'obfs-opts.path',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tfo
|
||||||
|
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||||
|
|
||||||
// udp
|
// udp
|
||||||
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
|
||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -461,7 +732,7 @@ function tuic(proxy) {
|
|||||||
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
|
||||||
|
|
||||||
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
|
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
|
||||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||||
result.appendIfPresent(`,token=${proxy.token}`, 'token');
|
result.appendIfPresent(`,token=${proxy.token}`, 'token');
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
@@ -469,11 +740,18 @@ function tuic(proxy) {
|
|||||||
'alpn',
|
'alpn',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isPresent(proxy, 'ports')) {
|
||||||
|
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
|
||||||
|
}
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
`,port-hopping-interval=${proxy['hop-interval']}`,
|
||||||
'ip-version',
|
'hop-interval',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
'no-error-alert',
|
'no-error-alert',
|
||||||
@@ -501,6 +779,21 @@ function tuic(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -531,6 +824,122 @@ function tuic(proxy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wireguard(proxy) {
|
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'];
|
||||||
|
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
|
||||||
|
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
|
||||||
|
proxy.reserved = proxy.peers[0].reserved;
|
||||||
|
}
|
||||||
|
const result = new Result(proxy);
|
||||||
|
|
||||||
|
result.append(`# > WireGuard Proxy ${proxy.name}
|
||||||
|
# ${proxy.name}=wireguard`);
|
||||||
|
|
||||||
|
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,section-name=${proxy['section-name']}`,
|
||||||
|
'section-name',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
|
'no-error-alert',
|
||||||
|
);
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
|
// test-url
|
||||||
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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.append(`
|
||||||
|
# > WireGuard Section ${proxy.name}
|
||||||
|
[WireGuard ${proxy['section-name']}]
|
||||||
|
private-key = ${proxy['private-key']}`);
|
||||||
|
|
||||||
|
result.appendIfPresent(`\nself-ip = ${proxy.ip}`, 'ip');
|
||||||
|
result.appendIfPresent(`\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6');
|
||||||
|
if (proxy.dns) {
|
||||||
|
if (Array.isArray(proxy.dns)) {
|
||||||
|
proxy.dns = proxy.dns.join(', ');
|
||||||
|
}
|
||||||
|
result.append(`\ndns-server = ${proxy.dns}`);
|
||||||
|
}
|
||||||
|
result.appendIfPresent(`\nmtu = ${proxy.mtu}`, 'mtu');
|
||||||
|
|
||||||
|
if (ip_version === 'prefer-v6') {
|
||||||
|
result.append(`\nprefer-ipv6 = true`);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||||
|
if (presharedKey) {
|
||||||
|
presharedKey = `,preshared-key="${presharedKey}"`;
|
||||||
|
}
|
||||||
|
const peer = {
|
||||||
|
'public-key': proxy['public-key'],
|
||||||
|
'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
|
||||||
|
endpoint: `${proxy.server}:${proxy.port}`,
|
||||||
|
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
|
||||||
|
'client-id': reserved,
|
||||||
|
'preshared-key': presharedKey,
|
||||||
|
};
|
||||||
|
result.append(
|
||||||
|
`\npeer = (${Object.keys(peer)
|
||||||
|
.filter((k) => peer[k] != null)
|
||||||
|
.map((k) => `${k} = ${peer[k]}`)
|
||||||
|
.join(', ')})`,
|
||||||
|
);
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
function wireguard_surge(proxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
|
|
||||||
result.append(`${proxy.name}=wireguard`);
|
result.append(`${proxy.name}=wireguard`);
|
||||||
@@ -544,13 +953,26 @@ function wireguard(proxy) {
|
|||||||
'no-error-alert',
|
'no-error-alert',
|
||||||
);
|
);
|
||||||
|
|
||||||
result.appendIfPresent(
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
'ip-version',
|
|
||||||
);
|
|
||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -585,13 +1007,20 @@ function hysteria2(proxy) {
|
|||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
|
||||||
|
|
||||||
result.appendIfPresent(`,password=${proxy.password}`, 'password');
|
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
|
||||||
|
|
||||||
|
if (isPresent(proxy, 'ports')) {
|
||||||
|
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
|
||||||
|
}
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
|
`,port-hopping-interval=${proxy['hop-interval']}`,
|
||||||
'ip-version',
|
'hop-interval',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||||
|
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
|
||||||
|
|
||||||
result.appendIfPresent(
|
result.appendIfPresent(
|
||||||
`,no-error-alert=${proxy['no-error-alert']}`,
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
'no-error-alert',
|
'no-error-alert',
|
||||||
@@ -617,6 +1046,21 @@ function hysteria2(proxy) {
|
|||||||
|
|
||||||
// test-url
|
// test-url
|
||||||
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,test-timeout=${proxy['test-timeout']}`,
|
||||||
|
'test-timeout',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
|
||||||
|
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
|
||||||
|
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,allow-other-interface=${proxy['allow-other-interface']}`,
|
||||||
|
'allow-other-interface',
|
||||||
|
);
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,interface=${proxy['interface-name']}`,
|
||||||
|
'interface-name',
|
||||||
|
);
|
||||||
|
|
||||||
// shadow-tls
|
// shadow-tls
|
||||||
if (isPresent(proxy, 'shadow-tls-password')) {
|
if (isPresent(proxy, 'shadow-tls-password')) {
|
||||||
@@ -652,7 +1096,7 @@ function hysteria2(proxy) {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransport(result, proxy) {
|
function handleTransport(result, proxy, includeUnsupportedProxy) {
|
||||||
if (isPresent(proxy, 'network')) {
|
if (isPresent(proxy, 'network')) {
|
||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
result.append(`,ws=true`);
|
result.append(`,ws=true`);
|
||||||
@@ -678,7 +1122,13 @@ function handleTransport(result, proxy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`network ${proxy.network} is unsupported`);
|
if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
|
||||||
|
$.info(
|
||||||
|
`Include Unsupported Proxy: nework ${proxy.network} -> tcp`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,183 @@
|
|||||||
import { Result } from './utils';
|
import { Base64 } from 'js-base64';
|
||||||
|
import { Result, isPresent } from './utils';
|
||||||
import Surge_Producer from './surge';
|
import Surge_Producer from './surge';
|
||||||
|
import ClashMeta_Producer from './clashmeta';
|
||||||
|
import { isIPv4, isIPv6 } from '@/utils';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
const targetPlatform = 'SurgeMac';
|
const targetPlatform = 'SurgeMac';
|
||||||
|
|
||||||
const surge_Producer = Surge_Producer();
|
const surge_Producer = Surge_Producer();
|
||||||
|
|
||||||
export default function SurgeMac_Producer() {
|
export default function SurgeMac_Producer() {
|
||||||
const produce = (proxy) => {
|
const produce = (proxy, type, opts = {}) => {
|
||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'ssr':
|
case 'external':
|
||||||
return shadowsocksr(proxy);
|
return external(proxy);
|
||||||
case 'ss':
|
// case 'ssr':
|
||||||
return surge_Producer.produce(proxy);
|
// return shadowsocksr(proxy);
|
||||||
case 'trojan':
|
default: {
|
||||||
return surge_Producer.produce(proxy);
|
try {
|
||||||
case 'vmess':
|
return surge_Producer.produce(proxy, type, opts);
|
||||||
return surge_Producer.produce(proxy);
|
} catch (e) {
|
||||||
case 'http':
|
if (opts.useMihomoExternal) {
|
||||||
return surge_Producer.produce(proxy);
|
$.log(
|
||||||
case 'socks5':
|
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
|
||||||
return surge_Producer.produce(proxy);
|
);
|
||||||
case 'snell':
|
return mihomo(proxy, type, opts);
|
||||||
return surge_Producer.produce(proxy);
|
} else {
|
||||||
case 'tuic':
|
throw new Error(
|
||||||
return surge_Producer.produce(proxy);
|
`Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new Error(
|
|
||||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
return { produce };
|
return { produce };
|
||||||
}
|
}
|
||||||
|
function external(proxy) {
|
||||||
function shadowsocksr(proxy) {
|
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
|
if (!proxy.exec || !proxy['local-port']) {
|
||||||
proxy.local_port = '__SubStoreLocalPort__';
|
throw new Error(`${proxy.type}: exec and local-port are required`);
|
||||||
proxy.local_address = proxy.local_address ?? '127.0.0.1';
|
}
|
||||||
|
|
||||||
result.append(
|
result.append(
|
||||||
`${proxy.name} = external, exec = "${
|
`${proxy.name}=external,exec="${proxy.exec}",local-port=${proxy['local-port']}`,
|
||||||
proxy.exec || '/usr/local/bin/ssr-local'
|
|
||||||
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(proxy.args)) {
|
||||||
|
proxy.args.map((args) => {
|
||||||
|
result.append(`,args="${args}"`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Array.isArray(proxy.addresses)) {
|
||||||
|
proxy.addresses.map((addresses) => {
|
||||||
|
result.append(`,addresses=${addresses}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.appendIfPresent(
|
||||||
|
`,no-error-alert=${proxy['no-error-alert']}`,
|
||||||
|
'no-error-alert',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
// block-quic
|
||||||
|
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function shadowsocksr(proxy) {
|
||||||
|
const external_proxy = {
|
||||||
|
...proxy,
|
||||||
|
type: 'external',
|
||||||
|
exec: proxy.exec || '/usr/local/bin/ssr-local',
|
||||||
|
'local-port': '__SubStoreLocalPort__',
|
||||||
|
args: [],
|
||||||
|
addresses: [],
|
||||||
|
'local-address':
|
||||||
|
proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1',
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://manual.nssurge.com/policy/external-proxy.html
|
||||||
|
if (isIP(proxy.server)) {
|
||||||
|
external_proxy.addresses.push(proxy.server);
|
||||||
|
} else {
|
||||||
|
$.log(
|
||||||
|
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries({
|
for (const [key, value] of Object.entries({
|
||||||
cipher: '-m',
|
cipher: '-m',
|
||||||
obfs: '-o',
|
obfs: '-o',
|
||||||
|
'obfs-param': '-g',
|
||||||
password: '-k',
|
password: '-k',
|
||||||
port: '-p',
|
port: '-p',
|
||||||
protocol: '-O',
|
protocol: '-O',
|
||||||
'protocol-param': '-G',
|
'protocol-param': '-G',
|
||||||
server: '-s',
|
server: '-s',
|
||||||
local_port: '-l',
|
'local-port': '-l',
|
||||||
local_address: '-b',
|
'local-address': '-b',
|
||||||
})) {
|
})) {
|
||||||
result.appendIfPresent(
|
if (external_proxy[key] != null) {
|
||||||
`, args = "${value}", args = "${proxy[key]}"`,
|
external_proxy.args.push(value);
|
||||||
key,
|
external_proxy.args.push(external_proxy[key]);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.toString();
|
return external(external_proxy);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function mihomo(proxy, type, opts) {
|
||||||
|
const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];
|
||||||
|
if (clashProxy) {
|
||||||
|
const localPort = opts?.localPort || proxy._localPort || 65535;
|
||||||
|
const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])
|
||||||
|
? false
|
||||||
|
: true;
|
||||||
|
const external_proxy = {
|
||||||
|
name: proxy.name,
|
||||||
|
type: 'external',
|
||||||
|
exec: proxy._exec || '/usr/local/bin/mihomo',
|
||||||
|
'local-port': localPort,
|
||||||
|
args: [
|
||||||
|
'-config',
|
||||||
|
Base64.encode(
|
||||||
|
JSON.stringify({
|
||||||
|
'mixed-port': localPort,
|
||||||
|
ipv6,
|
||||||
|
mode: 'global',
|
||||||
|
dns: {
|
||||||
|
enable: true,
|
||||||
|
ipv6,
|
||||||
|
nameserver: [
|
||||||
|
'https://223.6.6.6/dns-query',
|
||||||
|
'https://120.53.53.53/dns-query',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
proxies: [
|
||||||
|
{
|
||||||
|
...clashProxy,
|
||||||
|
name: 'proxy',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'proxy-groups': [
|
||||||
|
{
|
||||||
|
name: 'GLOBAL',
|
||||||
|
type: 'select',
|
||||||
|
proxies: ['proxy'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
addresses: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://manual.nssurge.com/policy/external-proxy.html
|
||||||
|
if (isIP(proxy.server)) {
|
||||||
|
external_proxy.addresses.push(proxy.server);
|
||||||
|
} else {
|
||||||
|
$.log(
|
||||||
|
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
opts.localPort = localPort - 1;
|
||||||
|
return external(external_proxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIP(ip) {
|
||||||
|
return isIPv4(ip) || isIPv6(ip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,41 @@ export default function URI_Producer() {
|
|||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
const produce = (proxy) => {
|
const produce = (proxy) => {
|
||||||
let result = '';
|
let result = '';
|
||||||
|
delete proxy.subName;
|
||||||
|
delete proxy.collectionName;
|
||||||
|
delete proxy.id;
|
||||||
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
|
for (const key in proxy) {
|
||||||
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
|
delete proxy[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
|
||||||
|
proxy.type,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete proxy.tls;
|
||||||
|
}
|
||||||
if (proxy.server && isIPv6(proxy.server)) {
|
if (proxy.server && isIPv6(proxy.server)) {
|
||||||
proxy.server = `[${proxy.server}]`;
|
proxy.server = `[${proxy.server}]`;
|
||||||
}
|
}
|
||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
|
case 'socks5':
|
||||||
|
result = `socks://${encodeURIComponent(
|
||||||
|
Base64.encode(`${proxy.username}:${proxy.password}`),
|
||||||
|
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
|
||||||
|
break;
|
||||||
case 'ss':
|
case 'ss':
|
||||||
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
||||||
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
|
result = `ss://${
|
||||||
proxy.port
|
proxy.cipher?.startsWith('2022-blake3-')
|
||||||
}/`;
|
? `${encodeURIComponent(
|
||||||
|
proxy.cipher,
|
||||||
|
)}:${encodeURIComponent(proxy.password)}`
|
||||||
|
: Base64.encode(userinfo)
|
||||||
|
}@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;
|
||||||
if (proxy.plugin) {
|
if (proxy.plugin) {
|
||||||
result += '?plugin=';
|
result += '?plugin=';
|
||||||
const opts = proxy['plugin-opts'];
|
const opts = proxy['plugin-opts'];
|
||||||
@@ -33,12 +59,25 @@ export default function URI_Producer() {
|
|||||||
}${opts.tls ? ';tls' : ''}`,
|
}${opts.tls ? ';tls' : ''}`,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'shadow-tls':
|
||||||
|
result += encodeURIComponent(
|
||||||
|
`shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unsupported plugin option: ${proxy.plugin}`,
|
`Unsupported plugin option: ${proxy.plugin}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (proxy['udp-over-tcp']) {
|
||||||
|
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
|
||||||
|
}
|
||||||
|
if (proxy.tfo) {
|
||||||
|
result = `${result}${
|
||||||
|
proxy.plugin || proxy['udp-over-tcp'] ? '&' : '?'
|
||||||
|
}tfo=1`;
|
||||||
|
}
|
||||||
result += `#${encodeURIComponent(proxy.name)}`;
|
result += `#${encodeURIComponent(proxy.name)}`;
|
||||||
break;
|
break;
|
||||||
case 'ssr':
|
case 'ssr':
|
||||||
@@ -64,6 +103,11 @@ export default function URI_Producer() {
|
|||||||
if (proxy.network === 'http') {
|
if (proxy.network === 'http') {
|
||||||
net = 'tcp';
|
net = 'tcp';
|
||||||
type = 'http';
|
type = 'http';
|
||||||
|
} else if (
|
||||||
|
proxy.network === 'ws' &&
|
||||||
|
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||||
|
) {
|
||||||
|
net = 'httpupgrade';
|
||||||
}
|
}
|
||||||
result = {
|
result = {
|
||||||
v: '2',
|
v: '2',
|
||||||
@@ -72,7 +116,7 @@ export default function URI_Producer() {
|
|||||||
port: proxy.port,
|
port: proxy.port,
|
||||||
id: proxy.uuid,
|
id: proxy.uuid,
|
||||||
type,
|
type,
|
||||||
aid: 0,
|
aid: proxy.alterId || 0,
|
||||||
net,
|
net,
|
||||||
tls: proxy.tls ? 'tls' : '',
|
tls: proxy.tls ? 'tls' : '',
|
||||||
};
|
};
|
||||||
@@ -104,6 +148,8 @@ export default function URI_Producer() {
|
|||||||
result.type =
|
result.type =
|
||||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||||
'gun';
|
'gun';
|
||||||
|
result.host =
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||||
@@ -113,6 +159,7 @@ export default function URI_Producer() {
|
|||||||
const isReality = proxy['reality-opts'];
|
const isReality = proxy['reality-opts'];
|
||||||
let sid = '';
|
let sid = '';
|
||||||
let pbk = '';
|
let pbk = '';
|
||||||
|
let spx = '';
|
||||||
if (isReality) {
|
if (isReality) {
|
||||||
security = 'reality';
|
security = 'reality';
|
||||||
const publicKey = proxy['reality-opts']?.['public-key'];
|
const publicKey = proxy['reality-opts']?.['public-key'];
|
||||||
@@ -123,6 +170,10 @@ export default function URI_Producer() {
|
|||||||
if (shortId) {
|
if (shortId) {
|
||||||
sid = `&sid=${encodeURIComponent(shortId)}`;
|
sid = `&sid=${encodeURIComponent(shortId)}`;
|
||||||
}
|
}
|
||||||
|
const spiderX = proxy['reality-opts']?.['_spider-x'];
|
||||||
|
if (spiderX) {
|
||||||
|
spx = `&spx=${encodeURIComponent(spiderX)}`;
|
||||||
|
}
|
||||||
} else if (proxy.tls) {
|
} else if (proxy.tls) {
|
||||||
security = 'tls';
|
security = 'tls';
|
||||||
}
|
}
|
||||||
@@ -152,14 +203,35 @@ export default function URI_Producer() {
|
|||||||
if (proxy.flow) {
|
if (proxy.flow) {
|
||||||
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
|
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
|
||||||
}
|
}
|
||||||
let vlessTransport = `&type=${encodeURIComponent(
|
let extra = '';
|
||||||
proxy.network,
|
if (proxy._extra) {
|
||||||
)}`;
|
extra = `&extra=${encodeURIComponent(proxy._extra)}`;
|
||||||
|
}
|
||||||
|
let mode = '';
|
||||||
|
if (proxy._mode) {
|
||||||
|
mode = `&mode=${encodeURIComponent(proxy._mode)}`;
|
||||||
|
}
|
||||||
|
let vlessType = proxy.network;
|
||||||
|
if (
|
||||||
|
proxy.network === 'ws' &&
|
||||||
|
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||||
|
) {
|
||||||
|
vlessType = 'httpupgrade';
|
||||||
|
}
|
||||||
|
|
||||||
|
let vlessTransport = `&type=${encodeURIComponent(vlessType)}`;
|
||||||
if (['grpc'].includes(proxy.network)) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
// https://github.com/XTLS/Xray-core/issues/91
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
vlessTransport += `&mode=${encodeURIComponent(
|
vlessTransport += `&mode=${encodeURIComponent(
|
||||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
|
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
|
||||||
)}`;
|
)}`;
|
||||||
|
const authority =
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
||||||
|
if (authority) {
|
||||||
|
vlessTransport += `&authority=${encodeURIComponent(
|
||||||
|
authority,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let vlessTransportServiceName =
|
let vlessTransportServiceName =
|
||||||
@@ -188,19 +260,60 @@ export default function URI_Producer() {
|
|||||||
vlessTransportServiceName,
|
vlessTransportServiceName,
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
if (proxy.network === 'kcp') {
|
||||||
|
if (proxy.seed) {
|
||||||
|
vlessTransport += `&seed=${encodeURIComponent(
|
||||||
|
proxy.seed,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (proxy.headerType) {
|
||||||
|
vlessTransport += `&headerType=${encodeURIComponent(
|
||||||
|
proxy.headerType,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result = `vless://${proxy.uuid}@${proxy.server}:${
|
result = `vless://${proxy.uuid}@${proxy.server}:${
|
||||||
proxy.port
|
proxy.port
|
||||||
}?${vlessTransport}&security=${encodeURIComponent(
|
}?security=${encodeURIComponent(
|
||||||
security,
|
security,
|
||||||
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
|
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}#${encodeURIComponent(
|
||||||
proxy.name,
|
proxy.name,
|
||||||
)}`;
|
)}`;
|
||||||
break;
|
break;
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
let trojanTransport = '';
|
let trojanTransport = '';
|
||||||
if (proxy.network) {
|
if (proxy.network) {
|
||||||
trojanTransport = `&type=${proxy.network}`;
|
let trojanType = proxy.network;
|
||||||
|
if (
|
||||||
|
proxy.network === 'ws' &&
|
||||||
|
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||||
|
) {
|
||||||
|
trojanType = 'httpupgrade';
|
||||||
|
}
|
||||||
|
trojanTransport = `&type=${encodeURIComponent(trojanType)}`;
|
||||||
|
if (['grpc'].includes(proxy.network)) {
|
||||||
|
let trojanTransportServiceName =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
`${proxy.network}-service-name`
|
||||||
|
];
|
||||||
|
let trojanTransportAuthority =
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
||||||
|
if (trojanTransportServiceName) {
|
||||||
|
trojanTransport += `&serviceName=${encodeURIComponent(
|
||||||
|
trojanTransportServiceName,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (trojanTransportAuthority) {
|
||||||
|
trojanTransport += `&authority=${encodeURIComponent(
|
||||||
|
trojanTransportAuthority,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
trojanTransport += `&mode=${encodeURIComponent(
|
||||||
|
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||||
|
'gun',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
let trojanTransportPath =
|
let trojanTransportPath =
|
||||||
proxy[`${proxy.network}-opts`]?.path;
|
proxy[`${proxy.network}-opts`]?.path;
|
||||||
let trojanTransportHost =
|
let trojanTransportHost =
|
||||||
@@ -220,11 +333,57 @@ export default function URI_Producer() {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let trojanFp = '';
|
||||||
|
if (proxy['client-fingerprint']) {
|
||||||
|
trojanFp = `&fp=${encodeURIComponent(
|
||||||
|
proxy['client-fingerprint'],
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
let trojanAlpn = '';
|
||||||
|
if (proxy.alpn) {
|
||||||
|
trojanAlpn = `&alpn=${encodeURIComponent(
|
||||||
|
Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn
|
||||||
|
: proxy.alpn.join(','),
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
const trojanIsReality = proxy['reality-opts'];
|
||||||
|
let trojanSid = '';
|
||||||
|
let trojanPbk = '';
|
||||||
|
let trojanSpx = '';
|
||||||
|
let trojanSecurity = '';
|
||||||
|
let trojanMode = '';
|
||||||
|
let trojanExtra = '';
|
||||||
|
if (trojanIsReality) {
|
||||||
|
trojanSecurity = `&security=reality`;
|
||||||
|
const publicKey = proxy['reality-opts']?.['public-key'];
|
||||||
|
if (publicKey) {
|
||||||
|
trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;
|
||||||
|
}
|
||||||
|
const shortId = proxy['reality-opts']?.['short-id'];
|
||||||
|
if (shortId) {
|
||||||
|
trojanSid = `&sid=${encodeURIComponent(shortId)}`;
|
||||||
|
}
|
||||||
|
const spiderX = proxy['reality-opts']?.['_spider-x'];
|
||||||
|
if (spiderX) {
|
||||||
|
trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;
|
||||||
|
}
|
||||||
|
if (proxy._extra) {
|
||||||
|
trojanExtra = `&extra=${encodeURIComponent(
|
||||||
|
proxy._extra,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (proxy._mode) {
|
||||||
|
trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
result = `trojan://${proxy.password}@${proxy.server}:${
|
result = `trojan://${proxy.password}@${proxy.server}:${
|
||||||
proxy.port
|
proxy.port
|
||||||
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
|
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
|
||||||
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
|
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
|
||||||
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
|
}${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
|
||||||
|
proxy.name,
|
||||||
|
)}`;
|
||||||
break;
|
break;
|
||||||
case 'hysteria2':
|
case 'hysteria2':
|
||||||
let hysteria2params = [];
|
let hysteria2params = [];
|
||||||
@@ -248,6 +407,9 @@ export default function URI_Producer() {
|
|||||||
`sni=${encodeURIComponent(proxy.sni)}`,
|
`sni=${encodeURIComponent(proxy.sni)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (proxy.ports) {
|
||||||
|
hysteria2params.push(`mport=${proxy.ports}`);
|
||||||
|
}
|
||||||
if (proxy['tls-fingerprint']) {
|
if (proxy['tls-fingerprint']) {
|
||||||
hysteria2params.push(
|
hysteria2params.push(
|
||||||
`pinSHA256=${encodeURIComponent(
|
`pinSHA256=${encodeURIComponent(
|
||||||
@@ -264,6 +426,165 @@ export default function URI_Producer() {
|
|||||||
'&',
|
'&',
|
||||||
)}#${encodeURIComponent(proxy.name)}`;
|
)}#${encodeURIComponent(proxy.name)}`;
|
||||||
break;
|
break;
|
||||||
|
case 'hysteria':
|
||||||
|
let hysteriaParams = [];
|
||||||
|
Object.keys(proxy).forEach((key) => {
|
||||||
|
if (!['name', 'type', 'server', 'port'].includes(key)) {
|
||||||
|
const i = key.replace(/-/, '_');
|
||||||
|
if (['alpn'].includes(key)) {
|
||||||
|
if (proxy[key]) {
|
||||||
|
hysteriaParams.push(
|
||||||
|
`${i}=${encodeURIComponent(
|
||||||
|
Array.isArray(proxy[key])
|
||||||
|
? proxy[key][0]
|
||||||
|
: proxy[key],
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (['skip-cert-verify'].includes(key)) {
|
||||||
|
if (proxy[key]) {
|
||||||
|
hysteriaParams.push(`insecure=1`);
|
||||||
|
}
|
||||||
|
} else if (['tfo', 'fast-open'].includes(key)) {
|
||||||
|
if (
|
||||||
|
proxy[key] &&
|
||||||
|
!hysteriaParams.includes('fastopen=1')
|
||||||
|
) {
|
||||||
|
hysteriaParams.push(`fastopen=1`);
|
||||||
|
}
|
||||||
|
} else if (['ports'].includes(key)) {
|
||||||
|
hysteriaParams.push(`mport=${proxy[key]}`);
|
||||||
|
} else if (['auth-str'].includes(key)) {
|
||||||
|
hysteriaParams.push(`auth=${proxy[key]}`);
|
||||||
|
} else if (['up'].includes(key)) {
|
||||||
|
hysteriaParams.push(`upmbps=${proxy[key]}`);
|
||||||
|
} else if (['down'].includes(key)) {
|
||||||
|
hysteriaParams.push(`downmbps=${proxy[key]}`);
|
||||||
|
} else if (['_obfs'].includes(key)) {
|
||||||
|
hysteriaParams.push(`obfs=${proxy[key]}`);
|
||||||
|
} else if (['obfs'].includes(key)) {
|
||||||
|
hysteriaParams.push(`obfsParam=${proxy[key]}`);
|
||||||
|
} else if (['sni'].includes(key)) {
|
||||||
|
hysteriaParams.push(`peer=${proxy[key]}`);
|
||||||
|
} else if (proxy[key]) {
|
||||||
|
hysteriaParams.push(
|
||||||
|
`${i}=${encodeURIComponent(proxy[key])}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result = `hysteria://${proxy.server}:${
|
||||||
|
proxy.port
|
||||||
|
}?${hysteriaParams.join('&')}#${encodeURIComponent(
|
||||||
|
proxy.name,
|
||||||
|
)}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tuic':
|
||||||
|
if (!proxy.token || proxy.token.length === 0) {
|
||||||
|
let tuicParams = [];
|
||||||
|
Object.keys(proxy).forEach((key) => {
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'uuid',
|
||||||
|
'password',
|
||||||
|
'server',
|
||||||
|
'port',
|
||||||
|
].includes(key)
|
||||||
|
) {
|
||||||
|
const i = key.replace(/-/, '_');
|
||||||
|
if (['alpn'].includes(key)) {
|
||||||
|
if (proxy[key]) {
|
||||||
|
tuicParams.push(
|
||||||
|
`${i}=${encodeURIComponent(
|
||||||
|
Array.isArray(proxy[key])
|
||||||
|
? proxy[key][0]
|
||||||
|
: proxy[key],
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (['skip-cert-verify'].includes(key)) {
|
||||||
|
if (proxy[key]) {
|
||||||
|
tuicParams.push(`allow_insecure=1`);
|
||||||
|
}
|
||||||
|
} else if (['tfo', 'fast-open'].includes(key)) {
|
||||||
|
if (
|
||||||
|
proxy[key] &&
|
||||||
|
!tuicParams.includes('fast_open=1')
|
||||||
|
) {
|
||||||
|
tuicParams.push(`fast_open=1`);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
['disable-sni', 'reduce-rtt'].includes(key) &&
|
||||||
|
proxy[key]
|
||||||
|
) {
|
||||||
|
tuicParams.push(`${i.replace(/-/g, '_')}=1`);
|
||||||
|
} else if (proxy[key]) {
|
||||||
|
tuicParams.push(
|
||||||
|
`${i.replace(
|
||||||
|
/-/g,
|
||||||
|
'_',
|
||||||
|
)}=${encodeURIComponent(proxy[key])}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result = `tuic://${encodeURIComponent(
|
||||||
|
proxy.uuid,
|
||||||
|
)}:${encodeURIComponent(proxy.password)}@${proxy.server}:${
|
||||||
|
proxy.port
|
||||||
|
}?${tuicParams.join('&')}#${encodeURIComponent(
|
||||||
|
proxy.name,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'wireguard':
|
||||||
|
let wireguardParams = [];
|
||||||
|
|
||||||
|
Object.keys(proxy).forEach((key) => {
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'server',
|
||||||
|
'port',
|
||||||
|
'ip',
|
||||||
|
'ipv6',
|
||||||
|
'private-key',
|
||||||
|
].includes(key)
|
||||||
|
) {
|
||||||
|
if (['public-key'].includes(key)) {
|
||||||
|
wireguardParams.push(`publickey=${proxy[key]}`);
|
||||||
|
} else if (['udp'].includes(key)) {
|
||||||
|
if (proxy[key]) {
|
||||||
|
wireguardParams.push(`${key}=1`);
|
||||||
|
}
|
||||||
|
} else if (proxy[key]) {
|
||||||
|
wireguardParams.push(
|
||||||
|
`${key}=${encodeURIComponent(proxy[key])}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (proxy.ip && proxy.ipv6) {
|
||||||
|
wireguardParams.push(
|
||||||
|
`address=${proxy.ip}/32,${proxy.ipv6}/128`,
|
||||||
|
);
|
||||||
|
} else if (proxy.ip) {
|
||||||
|
wireguardParams.push(`address=${proxy.ip}/32`);
|
||||||
|
} else if (proxy.ipv6) {
|
||||||
|
wireguardParams.push(`address=${proxy.ipv6}/128`);
|
||||||
|
}
|
||||||
|
result = `wireguard://${encodeURIComponent(
|
||||||
|
proxy['private-key'],
|
||||||
|
)}@${proxy.server}:${proxy.port}/?${wireguardParams.join(
|
||||||
|
'&',
|
||||||
|
)}#${encodeURIComponent(proxy.name)}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import URI_Producer from './uri';
|
import URI_Producer from './uri';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
const URI = URI_Producer();
|
const URI = URI_Producer();
|
||||||
|
|
||||||
export default function V2Ray_Producer() {
|
export default function V2Ray_Producer() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const produce = (proxies) =>
|
const produce = (proxies) => {
|
||||||
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
|
let result = [];
|
||||||
|
proxies.map((proxy) => {
|
||||||
|
try {
|
||||||
|
result.push(URI.produce(proxy));
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Cannot produce proxy: ${JSON.stringify(
|
||||||
|
proxy,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\nReason: ${err}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Base64.encode(result.join('\n'));
|
||||||
|
};
|
||||||
|
|
||||||
return { type, produce };
|
return { type, produce };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const RULE_TYPES_MAPPING = [
|
|||||||
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
||||||
[/^PROTOCOL$/, 'PROTOCOL'],
|
[/^PROTOCOL$/, 'PROTOCOL'],
|
||||||
[/^IP-CIDR$/i, 'IP-CIDR'],
|
[/^IP-CIDR$/i, 'IP-CIDR'],
|
||||||
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
|
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
|
||||||
|
[/^GEOIP$/i, 'GEOIP'],
|
||||||
|
[/^GEOSITE$/i, 'GEOSITE'],
|
||||||
];
|
];
|
||||||
|
|
||||||
function AllRuleParser() {
|
function AllRuleParser() {
|
||||||
@@ -37,8 +39,7 @@ function AllRuleParser() {
|
|||||||
content: params[1],
|
content: params[1],
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
rule.type === 'IP-CIDR' ||
|
['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)
|
||||||
rule.type === 'IP-CIDR6'
|
|
||||||
) {
|
) {
|
||||||
rule.options = params.slice(2);
|
rule.options = params.slice(2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function HTML() {
|
|||||||
|
|
||||||
function ClashProvider() {
|
function ClashProvider() {
|
||||||
const name = 'Clash Provider';
|
const name = 'Clash Provider';
|
||||||
const test = (raw) => raw.indexOf('payload:') === 0;
|
const test = (raw) => /^payload:/gm.exec(raw).index >= 0;
|
||||||
const parse = (raw) => {
|
const parse = (raw) => {
|
||||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import YAML from 'static-js-yaml';
|
import YAML from '@/utils/yaml';
|
||||||
|
|
||||||
function QXFilter() {
|
function QXFilter() {
|
||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
@@ -10,6 +10,8 @@ function QXFilter() {
|
|||||||
'SRC-IP',
|
'SRC-IP',
|
||||||
'IN-PORT',
|
'IN-PORT',
|
||||||
'PROTOCOL',
|
'PROTOCOL',
|
||||||
|
'GEOSITE',
|
||||||
|
'GEOIP',
|
||||||
];
|
];
|
||||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||||
|
|
||||||
@@ -29,9 +31,12 @@ function QXFilter() {
|
|||||||
function SurgeRuleSet() {
|
function SurgeRuleSet() {
|
||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
const func = (rule) => {
|
const func = (rule) => {
|
||||||
|
const UNSUPPORTED = ['GEOSITE', 'GEOIP'];
|
||||||
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||||
let output = `${rule.type},${rule.content}`;
|
let output = `${rule.type},${rule.content}`;
|
||||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
|
||||||
output += rule.options ? `,${rule.options[0]}` : '';
|
output +=
|
||||||
|
rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
@@ -42,8 +47,14 @@ function LoonRules() {
|
|||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
const func = (rule) => {
|
const func = (rule) => {
|
||||||
// skip unsupported rules
|
// skip unsupported rules
|
||||||
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
|
const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];
|
||||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||||
|
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
|
||||||
|
// Loon only supports the no-resolve option
|
||||||
|
rule.options = rule.options.filter((option) =>
|
||||||
|
['no-resolve'].includes(option),
|
||||||
|
);
|
||||||
|
}
|
||||||
return SurgeRuleSet().func(rule);
|
return SurgeRuleSet().func(rule);
|
||||||
};
|
};
|
||||||
return { type, func };
|
return { type, func };
|
||||||
@@ -62,8 +73,17 @@ function ClashRuleProvider() {
|
|||||||
let output = `${TRANSFORM[rule.type] || rule.type},${
|
let output = `${TRANSFORM[rule.type] || rule.type},${
|
||||||
rule.content
|
rule.content
|
||||||
}`;
|
}`;
|
||||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
|
||||||
output += rule.options ? `,${rule.options[0]}` : '';
|
if (rule.options) {
|
||||||
|
// Clash only supports the no-resolve option
|
||||||
|
rule.options = rule.options.filter((option) =>
|
||||||
|
['no-resolve'].includes(option),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
output +=
|
||||||
|
rule.options?.length > 0
|
||||||
|
? `,${rule.options.join(',')}`
|
||||||
|
: '';
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,21 +1,75 @@
|
|||||||
import { version } from '../../package.json';
|
import { version } from '../../package.json';
|
||||||
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
|
import {
|
||||||
|
SETTINGS_KEY,
|
||||||
|
ARTIFACTS_KEY,
|
||||||
|
SUBS_KEY,
|
||||||
|
COLLECTIONS_KEY,
|
||||||
|
} from '@/constants';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { produceArtifact } from '@/restful/sync';
|
import { produceArtifact } from '@/restful/sync';
|
||||||
import { syncToGist } from '@/restful/artifacts';
|
import { syncToGist } from '@/restful/artifacts';
|
||||||
|
import { findByName } from '@/utils/database';
|
||||||
|
|
||||||
!(async function () {
|
!(async function () {
|
||||||
const settings = $.read(SETTINGS_KEY);
|
let arg;
|
||||||
// if GitHub token is not configured
|
if (typeof $argument != 'undefined') {
|
||||||
if (!settings.githubUser || !settings.gistToken) return;
|
arg = Object.fromEntries(
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
$argument.split('&').map((item) => item.split('=')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
arg = {};
|
||||||
|
}
|
||||||
|
let sub_names = (arg?.subscription ?? arg?.sub ?? '')
|
||||||
|
.split(/,|,/g)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length > 0)
|
||||||
|
.map((i) => decodeURIComponent(i));
|
||||||
|
let col_names = (arg?.collection ?? arg?.col ?? '')
|
||||||
|
.split(/,|,/g)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length > 0)
|
||||||
|
.map((i) => decodeURIComponent(i));
|
||||||
|
if (sub_names.length > 0 || col_names.length > 0) {
|
||||||
|
if (sub_names.length > 0)
|
||||||
|
await produceArtifacts(sub_names, 'subscription');
|
||||||
|
if (col_names.length > 0)
|
||||||
|
await produceArtifacts(col_names, 'collection');
|
||||||
|
} else {
|
||||||
|
const settings = $.read(SETTINGS_KEY);
|
||||||
|
// if GitHub token is not configured
|
||||||
|
if (!settings.githubUser || !settings.gistToken) return;
|
||||||
|
|
||||||
const artifacts = $.read(ARTIFACTS_KEY);
|
const artifacts = $.read(ARTIFACTS_KEY);
|
||||||
if (!artifacts || artifacts.length === 0) return;
|
if (!artifacts || artifacts.length === 0) return;
|
||||||
|
|
||||||
const shouldSync = artifacts.some((artifact) => artifact.sync);
|
const shouldSync = artifacts.some((artifact) => artifact.sync);
|
||||||
if (shouldSync) await doSync();
|
if (shouldSync) await doSync();
|
||||||
|
}
|
||||||
})().finally(() => $.done());
|
})().finally(() => $.done());
|
||||||
|
|
||||||
|
async function produceArtifacts(names, type) {
|
||||||
|
try {
|
||||||
|
if (names.length > 0) {
|
||||||
|
$.info(`produceArtifacts ${type} 开始: ${names.join(', ')}`);
|
||||||
|
await Promise.all(
|
||||||
|
names.map(async (name) => {
|
||||||
|
try {
|
||||||
|
await produceArtifact({
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`${type} ${name} error: ${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
$.info(`produceArtifacts ${type} 完成: ${names.join(', ')}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`produceArtifacts error: ${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
async function doSync() {
|
async function doSync() {
|
||||||
console.log(
|
console.log(
|
||||||
`
|
`
|
||||||
@@ -30,41 +84,158 @@ async function doSync() {
|
|||||||
const files = {};
|
const files = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const valid = [];
|
||||||
|
const invalid = [];
|
||||||
|
const allSubs = $.read(SUBS_KEY);
|
||||||
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
|
const subNames = [];
|
||||||
|
allArtifacts.map((artifact) => {
|
||||||
|
if (artifact.sync && artifact.source) {
|
||||||
|
if (artifact.type === 'subscription') {
|
||||||
|
const subName = artifact.source;
|
||||||
|
const sub = findByName(allSubs, subName);
|
||||||
|
if (sub && sub.url && !subNames.includes(subName)) {
|
||||||
|
subNames.push(subName);
|
||||||
|
}
|
||||||
|
} else if (artifact.type === 'collection') {
|
||||||
|
const collection = findByName(allCols, artifact.source);
|
||||||
|
if (collection && Array.isArray(collection.subscriptions)) {
|
||||||
|
collection.subscriptions.map((subName) => {
|
||||||
|
const sub = findByName(allSubs, subName);
|
||||||
|
if (sub && sub.url && !subNames.includes(subName)) {
|
||||||
|
subNames.push(subName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subNames.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
subNames.map(async (subName) => {
|
||||||
|
try {
|
||||||
|
await produceArtifact({
|
||||||
|
type: 'subscription',
|
||||||
|
name: subName,
|
||||||
|
awaitCustomCache: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// $.error(`${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
allArtifacts.map(async (artifact) => {
|
allArtifacts.map(async (artifact) => {
|
||||||
if (artifact.sync) {
|
try {
|
||||||
$.info(`正在同步云配置:${artifact.name}...`);
|
if (artifact.sync && artifact.source) {
|
||||||
const output = await produceArtifact({
|
$.info(`正在同步云配置:${artifact.name}...`);
|
||||||
type: artifact.type,
|
|
||||||
name: artifact.source,
|
|
||||||
platform: artifact.platform,
|
|
||||||
});
|
|
||||||
|
|
||||||
files[artifact.name] = {
|
const useMihomoExternal =
|
||||||
content: output,
|
artifact.platform === 'SurgeMac';
|
||||||
};
|
|
||||||
|
if (useMihomoExternal) {
|
||||||
|
$.info(
|
||||||
|
`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const output = await produceArtifact({
|
||||||
|
type: artifact.type,
|
||||||
|
name: artifact.source,
|
||||||
|
platform: artifact.platform,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy':
|
||||||
|
artifact.includeUnsupportedProxy,
|
||||||
|
useMihomoExternal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (!output || output.length === 0)
|
||||||
|
// throw new Error('该配置的结果为空 不进行上传');
|
||||||
|
|
||||||
|
files[encodeURIComponent(artifact.name)] = {
|
||||||
|
content: output,
|
||||||
|
};
|
||||||
|
|
||||||
|
valid.push(artifact.name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`生成同步配置 ${artifact.name} 发生错误: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
invalid.push(artifact.name);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
|
||||||
|
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
|
||||||
|
|
||||||
|
if (valid.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await syncToGist(files);
|
const resp = await syncToGist(files);
|
||||||
const body = JSON.parse(resp.body);
|
const body = JSON.parse(resp.body);
|
||||||
|
delete body.history;
|
||||||
|
delete body.forks;
|
||||||
|
delete body.owner;
|
||||||
|
Object.values(body.files).forEach((file) => {
|
||||||
|
delete file.content;
|
||||||
|
});
|
||||||
|
$.info('上传配置响应:');
|
||||||
|
$.info(JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
for (const artifact of allArtifacts) {
|
for (const artifact of allArtifacts) {
|
||||||
if (artifact.sync) {
|
if (
|
||||||
|
artifact.sync &&
|
||||||
|
artifact.source &&
|
||||||
|
valid.includes(artifact.name)
|
||||||
|
) {
|
||||||
artifact.updated = new Date().getTime();
|
artifact.updated = new Date().getTime();
|
||||||
// extract real url from gist
|
// extract real url from gist
|
||||||
artifact.url = body.files[artifact.name].raw_url.replace(
|
let files = body.files;
|
||||||
/\/raw\/[^/]*\/(.*)/,
|
let isGitLab;
|
||||||
'/raw/$1',
|
if (Array.isArray(files)) {
|
||||||
|
isGitLab = true;
|
||||||
|
files = Object.fromEntries(
|
||||||
|
files.map((item) => [item.path, item]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const raw_url =
|
||||||
|
files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||||
|
const new_url = isGitLab
|
||||||
|
? raw_url
|
||||||
|
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||||
|
$.info(
|
||||||
|
`上传配置完成\n文件列表: ${Object.keys(files).join(
|
||||||
|
', ',
|
||||||
|
)}\n当前文件: ${encodeURIComponent(
|
||||||
|
artifact.name,
|
||||||
|
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
|
||||||
);
|
);
|
||||||
|
artifact.url = new_url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
$.notify('🌍 Sub-Store', '全部订阅同步成功!');
|
$.info('上传配置成功');
|
||||||
} catch (err) {
|
|
||||||
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`);
|
if (invalid.length > 0) {
|
||||||
$.error(`无法同步订阅配置到 Gist,原因:${err}`);
|
$.notify(
|
||||||
|
'🌍 Sub-Store',
|
||||||
|
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$.notify('🌍 Sub-Store', '同步配置完成');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`);
|
||||||
|
$.error(`无法同步配置到 Gist,原因:${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,92 @@
|
|||||||
import { ProxyUtils } from '@/core/proxy-utils';
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
import { RuleUtils } from '@/core/rule-utils';
|
import { RuleUtils } from '@/core/rule-utils';
|
||||||
import { version } from '../../package.json';
|
import { version } from '../../package.json';
|
||||||
|
import download from '@/utils/download';
|
||||||
|
|
||||||
console.log(
|
let result = '';
|
||||||
`
|
let resource = typeof $resource !== 'undefined' ? $resource : '';
|
||||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
let resourceType = typeof $resourceType !== 'undefined' ? $resourceType : '';
|
||||||
Sub-Store -- v${version}
|
let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
|
||||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const RESOURCE_TYPE = {
|
!(async () => {
|
||||||
PROXY: 1,
|
console.log(
|
||||||
RULE: 2,
|
`
|
||||||
};
|
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||||
|
Sub-Store -- v${version}
|
||||||
|
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
let result = $resource;
|
let arg;
|
||||||
|
if (typeof $argument != 'undefined') {
|
||||||
|
arg = Object.fromEntries(
|
||||||
|
$argument.split('&').map((item) => item.split('=')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
arg = {};
|
||||||
|
}
|
||||||
|
|
||||||
if ($resourceType === RESOURCE_TYPE.PROXY) {
|
const RESOURCE_TYPE = {
|
||||||
const proxies = ProxyUtils.parse($resource);
|
PROXY: 1,
|
||||||
result = ProxyUtils.produce(proxies, 'Loon');
|
RULE: 2,
|
||||||
} else if ($resourceType === RESOURCE_TYPE.RULE) {
|
};
|
||||||
const rules = RuleUtils.parse($resource);
|
|
||||||
result = RuleUtils.produce(rules, 'Loon');
|
|
||||||
}
|
|
||||||
|
|
||||||
$done(result);
|
result = resource;
|
||||||
|
|
||||||
|
if (resourceType === RESOURCE_TYPE.PROXY) {
|
||||||
|
try {
|
||||||
|
let proxies = ProxyUtils.parse(resource);
|
||||||
|
result = ProxyUtils.produce(proxies, 'Loon', undefined, {
|
||||||
|
'include-unsupported-proxy': arg?.includeUnsupportedProxy,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log('解析器: 使用 resource 出现错误');
|
||||||
|
console.log(e.message ?? e);
|
||||||
|
}
|
||||||
|
if ((!result || /^\s*$/.test(result)) && resourceUrl) {
|
||||||
|
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
|
||||||
|
try {
|
||||||
|
let raw = await download(
|
||||||
|
resourceUrl,
|
||||||
|
arg?.ua,
|
||||||
|
arg?.timeout,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let proxies = ProxyUtils.parse(raw);
|
||||||
|
result = ProxyUtils.produce(proxies, 'Loon', undefined, {
|
||||||
|
'include-unsupported-proxy': arg?.includeUnsupportedProxy,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message ?? e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (resourceType === RESOURCE_TYPE.RULE) {
|
||||||
|
try {
|
||||||
|
const rules = RuleUtils.parse(resource);
|
||||||
|
result = RuleUtils.produce(rules, 'Loon');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message ?? e);
|
||||||
|
}
|
||||||
|
if ((!result || /^\s*$/.test(result)) && resourceUrl) {
|
||||||
|
console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);
|
||||||
|
try {
|
||||||
|
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
|
||||||
|
let rules = RuleUtils.parse(raw);
|
||||||
|
result = RuleUtils.produce(rules, 'Loon');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message ?? e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.log('解析器: 出现错误');
|
||||||
|
console.log(e.message ?? e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
$done(result || '');
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import registerSettingRoutes from '@/restful/settings';
|
|||||||
import registerMiscRoutes from '@/restful/miscs';
|
import registerMiscRoutes from '@/restful/miscs';
|
||||||
import registerSortRoutes from '@/restful/sort';
|
import registerSortRoutes from '@/restful/sort';
|
||||||
import registerFileRoutes from '@/restful/file';
|
import registerFileRoutes from '@/restful/file';
|
||||||
|
import registerTokenRoutes from '@/restful/token';
|
||||||
import registerModuleRoutes from '@/restful/module';
|
import registerModuleRoutes from '@/restful/module';
|
||||||
|
|
||||||
migrate();
|
migrate();
|
||||||
@@ -32,6 +33,7 @@ function serve() {
|
|||||||
// register routes
|
// register routes
|
||||||
registerCollectionRoutes($app);
|
registerCollectionRoutes($app);
|
||||||
registerSubscriptionRoutes($app);
|
registerSubscriptionRoutes($app);
|
||||||
|
registerTokenRoutes($app);
|
||||||
registerFileRoutes($app);
|
registerFileRoutes($app);
|
||||||
registerModuleRoutes($app);
|
registerModuleRoutes($app);
|
||||||
registerArtifactRoutes($app);
|
registerArtifactRoutes($app);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export default function register($app) {
|
|||||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||||
|
|
||||||
// RESTful APIs
|
// RESTful APIs
|
||||||
|
$app.get('/api/artifacts/restore', restoreArtifacts);
|
||||||
|
|
||||||
$app.route('/api/artifacts')
|
$app.route('/api/artifacts')
|
||||||
.get(getAllArtifacts)
|
.get(getAllArtifacts)
|
||||||
.post(createArtifact)
|
.post(createArtifact)
|
||||||
@@ -30,6 +32,73 @@ export default function register($app) {
|
|||||||
.delete(deleteArtifact);
|
.delete(deleteArtifact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function restoreArtifacts(_, res) {
|
||||||
|
$.info('开始恢复远程配置...');
|
||||||
|
try {
|
||||||
|
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||||
|
if (!gistToken) {
|
||||||
|
return Promise.reject('未设置 GitHub Token!');
|
||||||
|
}
|
||||||
|
const manager = new Gist({
|
||||||
|
token: gistToken,
|
||||||
|
key: ARTIFACT_REPOSITORY_KEY,
|
||||||
|
syncPlatform,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gist = await manager.locate();
|
||||||
|
if (!gist?.files) {
|
||||||
|
throw new Error(`找不到 Sub-Store Gist 文件列表`);
|
||||||
|
}
|
||||||
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
|
const failed = [];
|
||||||
|
Object.keys(gist.files).map((key) => {
|
||||||
|
const filename = gist.files[key]?.filename;
|
||||||
|
if (filename) {
|
||||||
|
if (encodeURIComponent(filename) !== filename) {
|
||||||
|
$.error(`文件名 ${filename} 未编码 不保存`);
|
||||||
|
failed.push(filename);
|
||||||
|
} else {
|
||||||
|
const artifact = findByName(allArtifacts, filename);
|
||||||
|
if (artifact) {
|
||||||
|
updateByName(allArtifacts, filename, {
|
||||||
|
...artifact,
|
||||||
|
url: gist.files[key]?.raw_url.replace(
|
||||||
|
/\/raw\/[^/]*\/(.*)/,
|
||||||
|
'/raw/$1',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
allArtifacts.push({
|
||||||
|
name: `${filename}`,
|
||||||
|
url: gist.files[key]?.raw_url.replace(
|
||||||
|
/\/raw\/[^/]*\/(.*)/,
|
||||||
|
'/raw/$1',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
|
} catch (err) {
|
||||||
|
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
success(res);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`恢复远程配置失败,原因:${e.message ?? e}`);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
`FAILED_TO_RESTORE_ARTIFACTS`,
|
||||||
|
`Failed to restore artifacts`,
|
||||||
|
`Reason: ${e.message ?? e}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAllArtifacts(req, res) {
|
function getAllArtifacts(req, res) {
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
success(res, allArtifacts);
|
success(res, allArtifacts);
|
||||||
@@ -140,6 +209,12 @@ async function deleteArtifact(req, res) {
|
|||||||
files[encodeURIComponent(artifact.name)] = {
|
files[encodeURIComponent(artifact.name)] = {
|
||||||
content: '',
|
content: '',
|
||||||
};
|
};
|
||||||
|
if (encodeURIComponent(artifact.name) !== artifact.name) {
|
||||||
|
files[artifact.name] = {
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
|
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
|
||||||
try {
|
try {
|
||||||
await syncToGist(files);
|
await syncToGist(files);
|
||||||
@@ -169,15 +244,34 @@ function validateArtifactName(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncToGist(files) {
|
async function syncToGist(files) {
|
||||||
const { gistToken } = $.read(SETTINGS_KEY);
|
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||||
if (!gistToken) {
|
if (!gistToken) {
|
||||||
return Promise.reject('未设置Gist Token!');
|
return Promise.reject('未设置 GitHub Token!');
|
||||||
}
|
}
|
||||||
const manager = new Gist({
|
const manager = new Gist({
|
||||||
token: gistToken,
|
token: gistToken,
|
||||||
key: ARTIFACT_REPOSITORY_KEY,
|
key: ARTIFACT_REPOSITORY_KEY,
|
||||||
|
syncPlatform,
|
||||||
});
|
});
|
||||||
return manager.upload(files);
|
const res = await manager.upload(files);
|
||||||
|
let body = {};
|
||||||
|
try {
|
||||||
|
body = JSON.parse(res.body);
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const url = body?.html_url ?? body?.web_url;
|
||||||
|
const settings = $.read(SETTINGS_KEY);
|
||||||
|
if (url) {
|
||||||
|
$.log(`同步 Gist 后, 找到 Sub-Store Gist: ${url}`);
|
||||||
|
settings.artifactStore = url;
|
||||||
|
settings.artifactStoreStatus = 'VALID';
|
||||||
|
} else {
|
||||||
|
$.error(`同步 Gist 后, 找不到 Sub-Store Gist`);
|
||||||
|
settings.artifactStoreStatus = 'NOT FOUND';
|
||||||
|
}
|
||||||
|
$.write(settings, SETTINGS_KEY);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { syncToGist };
|
export { syncToGist };
|
||||||
|
|||||||
@@ -50,11 +50,32 @@ function createCollection(req, res) {
|
|||||||
|
|
||||||
function getCollection(req, res) {
|
function getCollection(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
|
let { raw } = req.query;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
const collection = findByName(allCols, name);
|
const collection = findByName(allCols, name);
|
||||||
if (collection) {
|
if (collection) {
|
||||||
success(res, collection);
|
if (raw) {
|
||||||
|
res.set('content-type', 'application/json')
|
||||||
|
.set(
|
||||||
|
'content-disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(
|
||||||
|
`sub-store_collection_${name}_${new Date()
|
||||||
|
.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
})
|
||||||
|
.replace(/\D/g, '')}.json`,
|
||||||
|
)}"`,
|
||||||
|
)
|
||||||
|
.send(JSON.stringify(collection));
|
||||||
|
} else {
|
||||||
|
success(res, collection);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
|
|||||||
@@ -1,48 +1,204 @@
|
|||||||
import { getPlatformFromHeaders } from '@/utils/platform';
|
import {
|
||||||
|
getPlatformFromHeaders,
|
||||||
|
shouldIncludeUnsupportedProxy,
|
||||||
|
} from '@/utils/user-agent';
|
||||||
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
||||||
import { findByName } from '@/utils/database';
|
import { findByName } from '@/utils/database';
|
||||||
import { getFlowHeaders } from '@/utils/flow';
|
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { failed } from '@/restful/response';
|
import { failed } from '@/restful/response';
|
||||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||||
import { produceArtifact } from '@/restful/sync';
|
import { produceArtifact } from '@/restful/sync';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { isIPv4, isIPv6 } from '@/utils';
|
||||||
|
import { getISO } from '@/utils/geo';
|
||||||
|
import env from '@/utils/env';
|
||||||
|
|
||||||
export default function register($app) {
|
export default function register($app) {
|
||||||
|
$app.get('/share/col/:name/:target', async (req, res) => {
|
||||||
|
const { target } = req.params;
|
||||||
|
if (target) {
|
||||||
|
req.query.target = target;
|
||||||
|
$.info(`使用路由指定目标: ${target}`);
|
||||||
|
}
|
||||||
|
await downloadCollection(req, res);
|
||||||
|
});
|
||||||
|
$app.get('/share/col/:name', downloadCollection);
|
||||||
|
$app.get('/share/sub/:name/:target', async (req, res) => {
|
||||||
|
const { target } = req.params;
|
||||||
|
if (target) {
|
||||||
|
req.query.target = target;
|
||||||
|
$.info(`使用路由指定目标: ${target}`);
|
||||||
|
}
|
||||||
|
await downloadSubscription(req, res);
|
||||||
|
});
|
||||||
|
$app.get('/share/sub/:name', downloadSubscription);
|
||||||
|
|
||||||
|
$app.get('/download/collection/:name/:target', async (req, res) => {
|
||||||
|
const { target } = req.params;
|
||||||
|
if (target) {
|
||||||
|
req.query.target = target;
|
||||||
|
$.info(`使用路由指定目标: ${target}`);
|
||||||
|
}
|
||||||
|
await downloadCollection(req, res);
|
||||||
|
});
|
||||||
$app.get('/download/collection/:name', downloadCollection);
|
$app.get('/download/collection/:name', downloadCollection);
|
||||||
|
$app.get('/download/:name/:target', async (req, res) => {
|
||||||
|
const { target } = req.params;
|
||||||
|
if (target) {
|
||||||
|
req.query.target = target;
|
||||||
|
$.info(`使用路由指定目标: ${target}`);
|
||||||
|
}
|
||||||
|
await downloadSubscription(req, res);
|
||||||
|
});
|
||||||
$app.get('/download/:name', downloadSubscription);
|
$app.get('/download/:name', downloadSubscription);
|
||||||
|
|
||||||
|
$app.get(
|
||||||
|
'/download/collection/:name/api/v1/server/details',
|
||||||
|
async (req, res) => {
|
||||||
|
req.query.platform = 'JSON';
|
||||||
|
req.query.produceType = 'internal';
|
||||||
|
req.query.resultFormat = 'nezha';
|
||||||
|
await downloadCollection(req, res);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$app.get('/download/:name/api/v1/server/details', async (req, res) => {
|
||||||
|
req.query.platform = 'JSON';
|
||||||
|
req.query.produceType = 'internal';
|
||||||
|
req.query.resultFormat = 'nezha';
|
||||||
|
await downloadSubscription(req, res);
|
||||||
|
});
|
||||||
|
$app.get(
|
||||||
|
'/download/collection/:name/api/v1/monitor/:nezhaIndex',
|
||||||
|
async (req, res) => {
|
||||||
|
req.query.platform = 'JSON';
|
||||||
|
req.query.produceType = 'internal';
|
||||||
|
req.query.resultFormat = 'nezha-monitor';
|
||||||
|
await downloadCollection(req, res);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$app.get('/download/:name/api/v1/monitor/:nezhaIndex', async (req, res) => {
|
||||||
|
req.query.platform = 'JSON';
|
||||||
|
req.query.produceType = 'internal';
|
||||||
|
req.query.resultFormat = 'nezha-monitor';
|
||||||
|
await downloadSubscription(req, res);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadSubscription(req, res) {
|
async function downloadSubscription(req, res) {
|
||||||
let { name } = req.params;
|
let { name, nezhaIndex } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
nezhaIndex = decodeURIComponent(nezhaIndex);
|
||||||
|
|
||||||
|
const useMihomoExternal = req.query.target === 'SurgeMac';
|
||||||
|
|
||||||
const platform =
|
const platform =
|
||||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||||
|
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
|
||||||
$.info(`正在下载订阅:${name}`);
|
$.info(
|
||||||
let { url, ua, content, mergeSources } = req.query;
|
`正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
|
||||||
|
);
|
||||||
|
let {
|
||||||
|
url,
|
||||||
|
ua,
|
||||||
|
content,
|
||||||
|
mergeSources,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
includeUnsupportedProxy,
|
||||||
|
resultFormat,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
|
} = req.query;
|
||||||
|
let $options = {};
|
||||||
|
if (req.query.$options) {
|
||||||
|
try {
|
||||||
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||||
|
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||||||
|
} catch (e) {
|
||||||
|
for (const pair of req.query.$options.split('&')) {
|
||||||
|
const key = pair.split('=')[0];
|
||||||
|
const value = pair.split('=')[1];
|
||||||
|
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||||
|
$options[key] =
|
||||||
|
value == null || value === ''
|
||||||
|
? true
|
||||||
|
: decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||||||
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
url = decodeURIComponent(url);
|
url = decodeURIComponent(url);
|
||||||
$.info(`指定远程订阅 URL: ${url}`);
|
$.info(`指定远程订阅 URL: ${url}`);
|
||||||
}
|
if (!/^https?:\/\//.test(url)) {
|
||||||
if (ua) {
|
content = url;
|
||||||
ua = decodeURIComponent(ua);
|
$.info(`URL 不是链接,视为本地订阅`);
|
||||||
$.info(`指定远程订阅 User-Agent: ${ua}`);
|
}
|
||||||
}
|
}
|
||||||
if (content) {
|
if (content) {
|
||||||
content = decodeURIComponent(content);
|
content = decodeURIComponent(content);
|
||||||
$.info(`指定本地订阅: ${content}`);
|
$.info(`指定本地订阅: ${content}`);
|
||||||
}
|
}
|
||||||
|
if (proxy) {
|
||||||
|
proxy = decodeURIComponent(proxy);
|
||||||
|
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||||||
|
}
|
||||||
|
if (ua) {
|
||||||
|
ua = decodeURIComponent(ua);
|
||||||
|
$.info(`指定远程订阅 User-Agent: ${ua}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (mergeSources) {
|
if (mergeSources) {
|
||||||
mergeSources = decodeURIComponent(mergeSources);
|
mergeSources = decodeURIComponent(mergeSources);
|
||||||
$.info(`指定合并来源: ${mergeSources}`);
|
$.info(`指定合并来源: ${mergeSources}`);
|
||||||
}
|
}
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||||
|
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||||
|
}
|
||||||
|
if (produceType) {
|
||||||
|
produceType = decodeURIComponent(produceType);
|
||||||
|
$.info(`指定生产类型: ${produceType}`);
|
||||||
|
}
|
||||||
|
if (includeUnsupportedProxy) {
|
||||||
|
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
|
||||||
|
$.info(
|
||||||
|
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!includeUnsupportedProxy &&
|
||||||
|
shouldIncludeUnsupportedProxy(platform, reqUA)
|
||||||
|
) {
|
||||||
|
includeUnsupportedProxy = true;
|
||||||
|
$.info(
|
||||||
|
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useMihomoExternal) {
|
||||||
|
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noCache) {
|
||||||
|
$.info(`指定不使用缓存: ${noCache}`);
|
||||||
|
}
|
||||||
|
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
if (sub) {
|
if (sub) {
|
||||||
try {
|
try {
|
||||||
const output = await produceArtifact({
|
const passThroughUA = sub.passThroughUA;
|
||||||
|
if (passThroughUA) {
|
||||||
|
$.info(
|
||||||
|
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
|
||||||
|
);
|
||||||
|
ua = reqUA;
|
||||||
|
}
|
||||||
|
let output = await produceArtifact({
|
||||||
type: 'subscription',
|
type: 'subscription',
|
||||||
name,
|
name,
|
||||||
platform,
|
platform,
|
||||||
@@ -50,14 +206,64 @@ async function downloadSubscription(req, res) {
|
|||||||
ua,
|
ua,
|
||||||
content,
|
content,
|
||||||
mergeSources,
|
mergeSources,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||||
|
useMihomoExternal,
|
||||||
|
},
|
||||||
|
$options,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
});
|
});
|
||||||
|
let flowInfo;
|
||||||
if (sub.source !== 'local' || url) {
|
if (
|
||||||
|
sub.source !== 'local' ||
|
||||||
|
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// forward flow headers
|
url =
|
||||||
const flowInfo = await getFlowHeaders(url || sub.url);
|
`${url || sub.url}`
|
||||||
if (flowInfo) {
|
.split(/[\r\n]+/)
|
||||||
res.set('subscription-userinfo', flowInfo);
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)?.[0] || '';
|
||||||
|
|
||||||
|
let $arguments = {};
|
||||||
|
const rawArgs = url.split('#');
|
||||||
|
url = url.split('#')[0];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$arguments.noFlow) {
|
||||||
|
// forward flow headers
|
||||||
|
flowInfo = await getFlowHeaders(
|
||||||
|
$arguments?.insecure ? `${url}#insecure` : url,
|
||||||
|
$arguments.flowUserAgent,
|
||||||
|
undefined,
|
||||||
|
proxy || sub.proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
|
);
|
||||||
|
if (flowInfo) {
|
||||||
|
res.set(
|
||||||
|
'subscription-userinfo',
|
||||||
|
normalizeFlowHeader(flowInfo),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
@@ -67,8 +273,48 @@ async function downloadSubscription(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (sub.subUserinfo) {
|
||||||
|
let subUserInfo;
|
||||||
|
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||||
|
try {
|
||||||
|
subUserInfo = await getFlowHeaders(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
proxy || sub.proxy,
|
||||||
|
sub.subUserinfo,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`订阅 ${name} 使用自定义流量链接 ${
|
||||||
|
sub.subUserinfo
|
||||||
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subUserInfo = sub.subUserinfo;
|
||||||
|
}
|
||||||
|
res.set(
|
||||||
|
'subscription-userinfo',
|
||||||
|
normalizeFlowHeader(
|
||||||
|
[subUserInfo, flowInfo].filter((i) => i).join(';'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (platform === 'JSON') {
|
if (platform === 'JSON') {
|
||||||
|
if (resultFormat === 'nezha') {
|
||||||
|
output = nezhaTransform(output);
|
||||||
|
} else if (resultFormat === 'nezha-monitor') {
|
||||||
|
nezhaIndex = /^\d+$/.test(nezhaIndex)
|
||||||
|
? parseInt(nezhaIndex, 10)
|
||||||
|
: output.findIndex((i) => i.name === nezhaIndex);
|
||||||
|
output = await nezhaMonitor(
|
||||||
|
output[nezhaIndex],
|
||||||
|
nezhaIndex,
|
||||||
|
req.query,
|
||||||
|
);
|
||||||
|
}
|
||||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||||
output,
|
output,
|
||||||
);
|
);
|
||||||
@@ -92,7 +338,7 @@ async function downloadSubscription(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
$.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}!`);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new ResourceNotFoundError(
|
new ResourceNotFoundError(
|
||||||
@@ -105,35 +351,149 @@ async function downloadSubscription(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadCollection(req, res) {
|
async function downloadCollection(req, res) {
|
||||||
let { name } = req.params;
|
let { name, nezhaIndex } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
nezhaIndex = decodeURIComponent(nezhaIndex);
|
||||||
|
|
||||||
|
const useMihomoExternal = req.query.target === 'SurgeMac';
|
||||||
|
|
||||||
const platform =
|
const platform =
|
||||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||||
|
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
const collection = findByName(allCols, name);
|
const collection = findByName(allCols, name);
|
||||||
|
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
|
||||||
|
$.info(
|
||||||
|
`正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
|
||||||
|
);
|
||||||
|
|
||||||
$.info(`正在下载组合订阅:${name}`);
|
let {
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
includeUnsupportedProxy,
|
||||||
|
resultFormat,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
let $options = {};
|
||||||
|
if (req.query.$options) {
|
||||||
|
try {
|
||||||
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||||
|
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||||||
|
} catch (e) {
|
||||||
|
for (const pair of req.query.$options.split('&')) {
|
||||||
|
const key = pair.split('=')[0];
|
||||||
|
const value = pair.split('=')[1];
|
||||||
|
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||||
|
$options[key] =
|
||||||
|
value == null || value === ''
|
||||||
|
? true
|
||||||
|
: decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy) {
|
||||||
|
proxy = decodeURIComponent(proxy);
|
||||||
|
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
|
||||||
|
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||||
|
}
|
||||||
|
if (produceType) {
|
||||||
|
produceType = decodeURIComponent(produceType);
|
||||||
|
$.info(`指定生产类型: ${produceType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeUnsupportedProxy) {
|
||||||
|
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
|
||||||
|
$.info(
|
||||||
|
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!includeUnsupportedProxy &&
|
||||||
|
shouldIncludeUnsupportedProxy(platform, reqUA)
|
||||||
|
) {
|
||||||
|
includeUnsupportedProxy = true;
|
||||||
|
$.info(
|
||||||
|
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (useMihomoExternal) {
|
||||||
|
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
|
||||||
|
}
|
||||||
|
if (noCache) {
|
||||||
|
$.info(`指定不使用缓存: ${noCache}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
try {
|
try {
|
||||||
const output = await produceArtifact({
|
let output = await produceArtifact({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
name,
|
name,
|
||||||
platform,
|
platform,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
produceType,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||||
|
useMihomoExternal,
|
||||||
|
},
|
||||||
|
$options,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
|
ua: reqUA,
|
||||||
});
|
});
|
||||||
|
let subUserInfoOfSub;
|
||||||
// forward flow header from the first subscription in this collection
|
// forward flow header from the first subscription in this collection
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const subnames = collection.subscriptions;
|
const subnames = collection.subscriptions;
|
||||||
if (subnames.length > 0) {
|
if (subnames.length > 0) {
|
||||||
const sub = findByName(allSubs, subnames[0]);
|
const sub = findByName(allSubs, subnames[0]);
|
||||||
if (sub.source !== 'local') {
|
if (
|
||||||
|
sub.source !== 'local' ||
|
||||||
|
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const flowInfo = await getFlowHeaders(sub.url);
|
let url =
|
||||||
if (flowInfo) {
|
`${sub.url}`
|
||||||
res.set('subscription-userinfo', flowInfo);
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)?.[0] || '';
|
||||||
|
|
||||||
|
let $arguments = {};
|
||||||
|
const rawArgs = url.split('#');
|
||||||
|
url = url.split('#')[0];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$arguments.noFlow) {
|
||||||
|
subUserInfoOfSub = await getFlowHeaders(
|
||||||
|
$arguments?.insecure ? `${url}#insecure` : url,
|
||||||
|
$arguments.flowUserAgent,
|
||||||
|
undefined,
|
||||||
|
proxy || sub.proxy || collection.proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(
|
||||||
@@ -143,9 +503,77 @@ async function downloadCollection(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (sub.subUserinfo) {
|
||||||
|
let subUserInfo;
|
||||||
|
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||||
|
try {
|
||||||
|
subUserInfo = await getFlowHeaders(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
proxy || sub.proxy,
|
||||||
|
sub.subUserinfo,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`组合订阅 ${name} 使用自定义流量链接 ${
|
||||||
|
sub.subUserinfo
|
||||||
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subUserInfo = sub.subUserinfo;
|
||||||
|
}
|
||||||
|
subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]
|
||||||
|
.filter((i) => i)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`);
|
||||||
|
|
||||||
|
let subUserInfoOfCol;
|
||||||
|
if (/^https?:\/\//.test(collection.subUserinfo)) {
|
||||||
|
try {
|
||||||
|
subUserInfoOfCol = await getFlowHeaders(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
proxy || collection.proxy,
|
||||||
|
collection.subUserinfo,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`组合订阅 ${name} 使用自定义流量链接 ${
|
||||||
|
collection.subUserinfo
|
||||||
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subUserInfoOfCol = collection.subUserinfo;
|
||||||
|
}
|
||||||
|
const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]
|
||||||
|
.filter((i) => i)
|
||||||
|
.join('; ');
|
||||||
|
if (subUserInfo) {
|
||||||
|
res.set(
|
||||||
|
'subscription-userinfo',
|
||||||
|
normalizeFlowHeader(subUserInfo),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (platform === 'JSON') {
|
if (platform === 'JSON') {
|
||||||
|
if (resultFormat === 'nezha') {
|
||||||
|
output = nezhaTransform(output);
|
||||||
|
} else if (resultFormat === 'nezha-monitor') {
|
||||||
|
nezhaIndex = /^\d+$/.test(nezhaIndex)
|
||||||
|
? parseInt(nezhaIndex, 10)
|
||||||
|
: output.findIndex((i) => i.name === nezhaIndex);
|
||||||
|
output = await nezhaMonitor(
|
||||||
|
output[nezhaIndex],
|
||||||
|
nezhaIndex,
|
||||||
|
req.query,
|
||||||
|
);
|
||||||
|
}
|
||||||
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||||
output,
|
output,
|
||||||
);
|
);
|
||||||
@@ -168,7 +596,7 @@ async function downloadCollection(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$.notify(
|
$.error(
|
||||||
`🌍 Sub-Store 下载组合订阅失败`,
|
`🌍 Sub-Store 下载组合订阅失败`,
|
||||||
`❌ 未找到组合订阅:${name}!`,
|
`❌ 未找到组合订阅:${name}!`,
|
||||||
);
|
);
|
||||||
@@ -182,3 +610,149 @@ async function downloadCollection(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function nezhaMonitor(proxy, index, query) {
|
||||||
|
const result = {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { isLoon, isSurge } = $.env;
|
||||||
|
if (!isLoon && !isSurge)
|
||||||
|
throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)');
|
||||||
|
const node = ProxyUtils.produce([proxy], isLoon ? 'Loon' : 'Surge');
|
||||||
|
if (!node) throw new Error('当前客户端不兼容此节点');
|
||||||
|
const monitors = proxy._monitors || [
|
||||||
|
{
|
||||||
|
name: 'Cloudflare',
|
||||||
|
url: 'http://cp.cloudflare.com/generate_204',
|
||||||
|
method: 'HEAD',
|
||||||
|
number: 3,
|
||||||
|
timeout: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Google',
|
||||||
|
url: 'http://www.google.com/generate_204',
|
||||||
|
method: 'HEAD',
|
||||||
|
number: 3,
|
||||||
|
timeout: 2000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const number =
|
||||||
|
query.number || Math.max(...monitors.map((i) => i.number)) || 3;
|
||||||
|
for (const monitor of monitors) {
|
||||||
|
const interval = 10 * 60 * 1000;
|
||||||
|
const data = {
|
||||||
|
monitor_id: monitors.indexOf(monitor),
|
||||||
|
server_id: index,
|
||||||
|
monitor_name: monitor.name,
|
||||||
|
server_name: proxy.name,
|
||||||
|
created_at: [],
|
||||||
|
avg_delay: [],
|
||||||
|
};
|
||||||
|
for (let index = 0; index < number; index++) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
try {
|
||||||
|
await $.http[(monitor.method || 'HEAD').toLowerCase()]({
|
||||||
|
timeout: monitor.timeout || 2000,
|
||||||
|
url: monitor.url,
|
||||||
|
'policy-descriptor': node,
|
||||||
|
node,
|
||||||
|
});
|
||||||
|
const latency = Date.now() - startedAt;
|
||||||
|
$.info(`${monitor.name} latency: ${latency}`);
|
||||||
|
data.avg_delay.push(latency);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(e);
|
||||||
|
data.avg_delay.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.created_at.push(
|
||||||
|
Date.now() - interval * (monitor.number - index - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.result.push(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(e);
|
||||||
|
result.result.push({
|
||||||
|
monitor_id: 0,
|
||||||
|
server_id: 0,
|
||||||
|
monitor_name: `❌ ${e.message ?? e}`,
|
||||||
|
server_name: proxy.name,
|
||||||
|
created_at: [Date.now()],
|
||||||
|
avg_delay: [0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
function nezhaTransform(output) {
|
||||||
|
const result = {
|
||||||
|
code: 0,
|
||||||
|
message: 'success',
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
output.map((proxy, index) => {
|
||||||
|
// 如果节点上有数据 就取节点上的数据
|
||||||
|
let CountryCode = proxy._geo?.countryCode || proxy._geo?.country;
|
||||||
|
// 简单判断下
|
||||||
|
if (!/^[a-z]{2}$/i.test(CountryCode)) {
|
||||||
|
CountryCode = getISO(proxy.name);
|
||||||
|
}
|
||||||
|
// 简单判断下
|
||||||
|
if (/^[a-z]{2}$/i.test(CountryCode)) {
|
||||||
|
// 如果节点上有数据 就取节点上的数据
|
||||||
|
let now = Math.round(new Date().getTime() / 1000);
|
||||||
|
let time = proxy._unavailable ? 0 : now;
|
||||||
|
|
||||||
|
const uptime = parseInt(proxy._uptime || 0, 10);
|
||||||
|
|
||||||
|
result.result.push({
|
||||||
|
id: index,
|
||||||
|
name: proxy.name,
|
||||||
|
tag: `${proxy._tag ?? ''}`,
|
||||||
|
last_active: time,
|
||||||
|
// 暂时不用处理 现在 VPings App 端的接口支持域名查询
|
||||||
|
// 其他场景使用 自己在 Sub-Store 加一步域名解析
|
||||||
|
valid_ip: proxy._IP || proxy.server,
|
||||||
|
ipv4: proxy._IPv4 || proxy.server,
|
||||||
|
ipv6: proxy._IPv6 || (isIPv6(proxy.server) ? proxy.server : ''),
|
||||||
|
host: {
|
||||||
|
Platform: 'Sub-Store',
|
||||||
|
PlatformVersion: env.version,
|
||||||
|
CPU: [],
|
||||||
|
MemTotal: 1024,
|
||||||
|
DiskTotal: 1024,
|
||||||
|
SwapTotal: 1024,
|
||||||
|
Arch: '',
|
||||||
|
Virtualization: '',
|
||||||
|
BootTime: now - uptime,
|
||||||
|
CountryCode, // 目前需要
|
||||||
|
Version: '0.0.1',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
CPU: 0,
|
||||||
|
MemUsed: 0,
|
||||||
|
SwapUsed: 0,
|
||||||
|
DiskUsed: 0,
|
||||||
|
NetInTransfer: 0,
|
||||||
|
NetOutTransfer: 0,
|
||||||
|
NetInSpeed: 0,
|
||||||
|
NetOutSpeed: 0,
|
||||||
|
Uptime: uptime,
|
||||||
|
Load1: 0,
|
||||||
|
Load5: 0,
|
||||||
|
Load15: 0,
|
||||||
|
TcpConnCount: 0,
|
||||||
|
UdpConnCount: 0,
|
||||||
|
ProcessCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||||
|
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||||||
import { FILES_KEY } from '@/constants';
|
import { FILES_KEY } from '@/constants';
|
||||||
import { failed, success } from '@/restful/response';
|
import { failed, success } from '@/restful/response';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
import {
|
||||||
|
RequestInvalidError,
|
||||||
|
ResourceNotFoundError,
|
||||||
|
InternalServerError,
|
||||||
|
} from '@/restful/errors';
|
||||||
|
import { produceArtifact } from '@/restful/sync';
|
||||||
|
|
||||||
export default function register($app) {
|
export default function register($app) {
|
||||||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||||
|
|
||||||
|
$app.get('/share/file/:name', getFile);
|
||||||
|
|
||||||
$app.route('/api/file/:name')
|
$app.route('/api/file/:name')
|
||||||
.get(getFile)
|
.get(getFile)
|
||||||
.patch(updateFile)
|
.patch(updateFile)
|
||||||
.delete(deleteFile);
|
.delete(deleteFile);
|
||||||
|
|
||||||
|
$app.route('/api/wholeFile/:name').get(getWholeFile);
|
||||||
|
|
||||||
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||||||
|
$app.route('/api/wholeFiles').get(getAllWholeFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// file API
|
// file API
|
||||||
@@ -37,13 +48,184 @@ function createFile(req, res) {
|
|||||||
success(res, file, 201);
|
success(res, file, 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFile(req, res) {
|
async function getFile(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
|
||||||
|
$.info(`正在下载文件:${name}`);
|
||||||
|
let {
|
||||||
|
url,
|
||||||
|
subInfoUrl,
|
||||||
|
subInfoUserAgent,
|
||||||
|
ua,
|
||||||
|
content,
|
||||||
|
mergeSources,
|
||||||
|
ignoreFailedRemoteFile,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
|
} = req.query;
|
||||||
|
let $options = {};
|
||||||
|
if (req.query.$options) {
|
||||||
|
try {
|
||||||
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||||
|
$options = JSON.parse(decodeURIComponent(req.query.$options));
|
||||||
|
} catch (e) {
|
||||||
|
for (const pair of req.query.$options.split('&')) {
|
||||||
|
const key = pair.split('=')[0];
|
||||||
|
const value = pair.split('=')[1];
|
||||||
|
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||||
|
$options[key] =
|
||||||
|
value == null || value === ''
|
||||||
|
? true
|
||||||
|
: decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.info(`传入 $options: ${JSON.stringify($options)}`);
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
url = decodeURIComponent(url);
|
||||||
|
$.info(`指定远程文件 URL: ${url}`);
|
||||||
|
}
|
||||||
|
if (proxy) {
|
||||||
|
proxy = decodeURIComponent(proxy);
|
||||||
|
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||||||
|
}
|
||||||
|
if (ua) {
|
||||||
|
ua = decodeURIComponent(ua);
|
||||||
|
$.info(`指定远程文件 User-Agent: ${ua}`);
|
||||||
|
}
|
||||||
|
if (subInfoUrl) {
|
||||||
|
subInfoUrl = decodeURIComponent(subInfoUrl);
|
||||||
|
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
|
||||||
|
}
|
||||||
|
if (subInfoUserAgent) {
|
||||||
|
subInfoUserAgent = decodeURIComponent(subInfoUserAgent);
|
||||||
|
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
|
||||||
|
}
|
||||||
|
if (content) {
|
||||||
|
content = decodeURIComponent(content);
|
||||||
|
$.info(`指定本地文件: ${content}`);
|
||||||
|
}
|
||||||
|
if (mergeSources) {
|
||||||
|
mergeSources = decodeURIComponent(mergeSources);
|
||||||
|
$.info(`指定合并来源: ${mergeSources}`);
|
||||||
|
}
|
||||||
|
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
|
||||||
|
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
|
||||||
|
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
|
||||||
|
}
|
||||||
|
if (noCache) {
|
||||||
|
$.info(`指定不使用缓存: ${noCache}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
const file = findByName(allFiles, name);
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const output = await produceArtifact({
|
||||||
|
type: 'file',
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
ua,
|
||||||
|
content,
|
||||||
|
mergeSources,
|
||||||
|
ignoreFailedRemoteFile,
|
||||||
|
$options,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
subInfoUrl = subInfoUrl || file.subInfoUrl;
|
||||||
|
if (subInfoUrl) {
|
||||||
|
// forward flow headers
|
||||||
|
const flowInfo = await getFlowHeaders(
|
||||||
|
subInfoUrl,
|
||||||
|
subInfoUserAgent || file.subInfoUserAgent,
|
||||||
|
undefined,
|
||||||
|
proxy || file.proxy,
|
||||||
|
);
|
||||||
|
if (flowInfo) {
|
||||||
|
res.set(
|
||||||
|
'subscription-userinfo',
|
||||||
|
normalizeFlowHeader(flowInfo),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
|
||||||
|
err,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.download) {
|
||||||
|
res.set(
|
||||||
|
'Content-Disposition',
|
||||||
|
`attachment; filename*=UTF-8''${encodeURIComponent(
|
||||||
|
file.displayName || file.name,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||||
|
output ?? '',
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
$.notify(
|
||||||
|
`🌍 Sub-Store 下载文件失败`,
|
||||||
|
`❌ 无法下载文件:${name}!`,
|
||||||
|
`🤔 原因:${err.message ?? err}`,
|
||||||
|
);
|
||||||
|
$.error(err.message ?? err);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
'INTERNAL_SERVER_ERROR',
|
||||||
|
`Failed to download file: ${name}`,
|
||||||
|
`Reason: ${err.message ?? err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}!`);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
'RESOURCE_NOT_FOUND',
|
||||||
|
`File ${name} does not exist!`,
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getWholeFile(req, res) {
|
||||||
|
let { name } = req.params;
|
||||||
|
let { raw } = req.query;
|
||||||
|
name = decodeURIComponent(name);
|
||||||
const allFiles = $.read(FILES_KEY);
|
const allFiles = $.read(FILES_KEY);
|
||||||
const file = findByName(allFiles, name);
|
const file = findByName(allFiles, name);
|
||||||
if (file) {
|
if (file) {
|
||||||
res.status(200).json(file.content);
|
if (raw) {
|
||||||
|
res.set('content-type', 'application/json')
|
||||||
|
.set(
|
||||||
|
'content-disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(
|
||||||
|
`sub-store_file_${name}_${new Date()
|
||||||
|
.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
})
|
||||||
|
.replace(/\D/g, '')}.json`,
|
||||||
|
)}"`,
|
||||||
|
)
|
||||||
|
.send(JSON.stringify(file));
|
||||||
|
} else {
|
||||||
|
success(res, file);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
@@ -102,6 +284,11 @@ function getAllFiles(req, res) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAllWholeFiles(req, res) {
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
success(res, allFiles);
|
||||||
|
}
|
||||||
|
|
||||||
function replaceFile(req, res) {
|
function replaceFile(req, res) {
|
||||||
const allFiles = req.body;
|
const allFiles = req.body;
|
||||||
$.write(allFiles, FILES_KEY);
|
$.write(allFiles, FILES_KEY);
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import express from '@/vendor/express';
|
import express from '@/vendor/express';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
import migrate from '@/utils/migration';
|
||||||
|
import download from '@/utils/download';
|
||||||
|
import { syncArtifacts, produceArtifact } from '@/restful/sync';
|
||||||
|
import { gistBackupAction } from '@/restful/miscs';
|
||||||
|
import { TOKENS_KEY } from '@/constants';
|
||||||
|
|
||||||
import registerSubscriptionRoutes from './subscriptions';
|
import registerSubscriptionRoutes from './subscriptions';
|
||||||
import registerCollectionRoutes from './collections';
|
import registerCollectionRoutes from './collections';
|
||||||
import registerArtifactRoutes from './artifacts';
|
import registerArtifactRoutes from './artifacts';
|
||||||
import registerFileRoutes from './file';
|
import registerFileRoutes from './file';
|
||||||
|
import registerTokenRoutes from './token';
|
||||||
import registerModuleRoutes from './module';
|
import registerModuleRoutes from './module';
|
||||||
import registerSyncRoutes from './sync';
|
import registerSyncRoutes from './sync';
|
||||||
import registerDownloadRoutes from './download';
|
import registerDownloadRoutes from './download';
|
||||||
@@ -13,13 +19,14 @@ import registerPreviewRoutes from './preview';
|
|||||||
import registerSortingRoutes from './sort';
|
import registerSortingRoutes from './sort';
|
||||||
import registerMiscRoutes from './miscs';
|
import registerMiscRoutes from './miscs';
|
||||||
import registerNodeInfoRoutes from './node-info';
|
import registerNodeInfoRoutes from './node-info';
|
||||||
|
import registerParserRoutes from './parser';
|
||||||
|
|
||||||
export default function serve() {
|
export default function serve() {
|
||||||
let port;
|
let port;
|
||||||
let host;
|
let host;
|
||||||
if ($.env.isNode) {
|
if ($.env.isNode) {
|
||||||
port = eval('process.env.SUB_STORE_BACKEND_API_PORT');
|
port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;
|
||||||
host = eval('process.env.SUB_STORE_BACKEND_API_HOST');
|
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
|
||||||
}
|
}
|
||||||
const $app = express({ substore: $, port, host });
|
const $app = express({ substore: $, port, host });
|
||||||
// register routes
|
// register routes
|
||||||
@@ -31,16 +38,136 @@ export default function serve() {
|
|||||||
registerSettingRoutes($app);
|
registerSettingRoutes($app);
|
||||||
registerArtifactRoutes($app);
|
registerArtifactRoutes($app);
|
||||||
registerFileRoutes($app);
|
registerFileRoutes($app);
|
||||||
|
registerTokenRoutes($app);
|
||||||
registerModuleRoutes($app);
|
registerModuleRoutes($app);
|
||||||
registerSyncRoutes($app);
|
registerSyncRoutes($app);
|
||||||
registerNodeInfoRoutes($app);
|
registerNodeInfoRoutes($app);
|
||||||
registerMiscRoutes($app);
|
registerMiscRoutes($app);
|
||||||
|
registerParserRoutes($app);
|
||||||
|
|
||||||
$app.start();
|
$app.start();
|
||||||
|
|
||||||
if ($.env.isNode) {
|
if ($.env.isNode) {
|
||||||
|
// Deprecated: SUB_STORE_BACKEND_CRON
|
||||||
|
const backend_sync_cron =
|
||||||
|
eval('process.env.SUB_STORE_BACKEND_SYNC_CRON') ||
|
||||||
|
eval('process.env.SUB_STORE_BACKEND_CRON');
|
||||||
|
if (backend_sync_cron) {
|
||||||
|
$.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
|
||||||
|
const { CronJob } = eval(`require("cron")`);
|
||||||
|
new CronJob(
|
||||||
|
backend_sync_cron,
|
||||||
|
async function () {
|
||||||
|
try {
|
||||||
|
$.info(`[SYNC CRON] ${backend_sync_cron} started`);
|
||||||
|
await syncArtifacts();
|
||||||
|
$.info(`[SYNC CRON] ${backend_sync_cron} finished`);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`[SYNC CRON] ${backend_sync_cron} error: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, // onTick
|
||||||
|
null, // onComplete
|
||||||
|
true, // start
|
||||||
|
// 'Asia/Shanghai' // timeZone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
|
||||||
|
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
|
||||||
|
const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
|
||||||
|
if (produce_cron) {
|
||||||
|
$.info(`[PRODUCE CRON] ${produce_cron} enabled`);
|
||||||
|
const { CronJob } = eval(`require("cron")`);
|
||||||
|
produce_cron.split(/\s*;\s*/).map((item) => {
|
||||||
|
const [cron, type, name] = item.split(/\s*,\s*/);
|
||||||
|
new CronJob(
|
||||||
|
cron.trim(),
|
||||||
|
async function () {
|
||||||
|
try {
|
||||||
|
$.info(
|
||||||
|
`[PRODUCE CRON] ${type} ${name} ${cron} started`,
|
||||||
|
);
|
||||||
|
await produceArtifact({ type, name });
|
||||||
|
$.info(
|
||||||
|
`[PRODUCE CRON] ${type} ${name} ${cron} finished`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`[PRODUCE CRON] ${type} ${name} ${cron} error: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, // onTick
|
||||||
|
null, // onComplete
|
||||||
|
true, // start
|
||||||
|
// 'Asia/Shanghai' // timeZone
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const backend_download_cron = eval(
|
||||||
|
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
|
||||||
|
);
|
||||||
|
if (backend_download_cron) {
|
||||||
|
$.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`);
|
||||||
|
const { CronJob } = eval(`require("cron")`);
|
||||||
|
new CronJob(
|
||||||
|
backend_download_cron,
|
||||||
|
async function () {
|
||||||
|
try {
|
||||||
|
$.info(
|
||||||
|
`[DOWNLOAD CRON] ${backend_download_cron} started`,
|
||||||
|
);
|
||||||
|
await gistBackupAction('download');
|
||||||
|
$.info(
|
||||||
|
`[DOWNLOAD CRON] ${backend_download_cron} finished`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`[DOWNLOAD CRON] ${backend_download_cron} error: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, // onTick
|
||||||
|
null, // onComplete
|
||||||
|
true, // start
|
||||||
|
// 'Asia/Shanghai' // timeZone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const backend_upload_cron = eval(
|
||||||
|
'process.env.SUB_STORE_BACKEND_UPLOAD_CRON',
|
||||||
|
);
|
||||||
|
if (backend_upload_cron) {
|
||||||
|
$.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`);
|
||||||
|
const { CronJob } = eval(`require("cron")`);
|
||||||
|
new CronJob(
|
||||||
|
backend_upload_cron,
|
||||||
|
async function () {
|
||||||
|
try {
|
||||||
|
$.info(`[UPLOAD CRON] ${backend_upload_cron} started`);
|
||||||
|
await gistBackupAction('upload');
|
||||||
|
$.info(`[UPLOAD CRON] ${backend_upload_cron} finished`);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`[UPLOAD CRON] ${backend_upload_cron} error: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, // onTick
|
||||||
|
null, // onComplete
|
||||||
|
true, // start
|
||||||
|
// 'Asia/Shanghai' // timeZone
|
||||||
|
);
|
||||||
|
}
|
||||||
const path = eval(`require("path")`);
|
const path = eval(`require("path")`);
|
||||||
const fs = eval(`require("fs")`);
|
const fs = eval(`require("fs")`);
|
||||||
|
const data_url = eval('process.env.SUB_STORE_DATA_URL');
|
||||||
|
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
|
||||||
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
|
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
|
||||||
const fe_host =
|
const fe_host =
|
||||||
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
|
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
|
||||||
@@ -52,31 +179,131 @@ export default function serve() {
|
|||||||
try {
|
try {
|
||||||
fs.accessSync(path.join(fe_abs_path, 'index.html'));
|
fs.accessSync(path.join(fe_abs_path, 'index.html'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
$.error(
|
||||||
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
|
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const express_ = eval(`require("express")`);
|
const express_ = eval(`require("express")`);
|
||||||
const history = eval(`require("connect-history-api-fallback")`);
|
const history = eval(`require("connect-history-api-fallback")`);
|
||||||
|
const { createProxyMiddleware } = eval(
|
||||||
|
`require("http-proxy-middleware")`,
|
||||||
|
);
|
||||||
|
|
||||||
const app = express_();
|
const app = express_();
|
||||||
|
|
||||||
const staticFileMiddleware = express_.static(fe_path);
|
const staticFileMiddleware = express_.static(fe_path);
|
||||||
|
|
||||||
|
let be_share_rewrite = '/share/:type/:name';
|
||||||
|
let be_api_rewrite = '';
|
||||||
|
let be_download_rewrite = '';
|
||||||
|
let be_api = '/api/';
|
||||||
|
let be_download = '/download/';
|
||||||
|
if (fe_be_path) {
|
||||||
|
if (!fe_be_path.startsWith('/')) {
|
||||||
|
throw new Error(
|
||||||
|
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
be_api_rewrite = `${
|
||||||
|
fe_be_path === '/' ? '' : fe_be_path
|
||||||
|
}${be_api}`;
|
||||||
|
be_download_rewrite = `${
|
||||||
|
fe_be_path === '/' ? '' : fe_be_path
|
||||||
|
}${be_download}`;
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
be_share_rewrite,
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: `http://127.0.0.1:${port}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: (path, req) => {
|
||||||
|
if (req.method.toLowerCase() !== 'get')
|
||||||
|
throw new Error('Method not allowed');
|
||||||
|
const tokens = $.read(TOKENS_KEY) || [];
|
||||||
|
const token = tokens.find(
|
||||||
|
(t) =>
|
||||||
|
t.token === req.query.token &&
|
||||||
|
t.type === req.params.type &&
|
||||||
|
t.name === req.params.name &&
|
||||||
|
(t.exp == null || t.exp > Date.now()),
|
||||||
|
);
|
||||||
|
if (!token) throw new Error('Forbbiden');
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
be_api_rewrite,
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: `http://127.0.0.1:${port}`,
|
||||||
|
pathRewrite: (path) => {
|
||||||
|
const newPath = path.startsWith(be_api_rewrite)
|
||||||
|
? path.replace(be_api_rewrite, be_api)
|
||||||
|
: path;
|
||||||
|
return newPath.includes('?')
|
||||||
|
? `${newPath}&share=true`
|
||||||
|
: `${newPath}?share=true`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
be_download_rewrite,
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: `http://127.0.0.1:${port}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: (path) => {
|
||||||
|
return path.startsWith(be_download_rewrite)
|
||||||
|
? path.replace(be_download_rewrite, be_download)
|
||||||
|
: path;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
app.use(staticFileMiddleware);
|
app.use(staticFileMiddleware);
|
||||||
app.use(
|
app.use(
|
||||||
history({
|
history({
|
||||||
disableDotRule: true,
|
disableDotRule: true,
|
||||||
verbose: true,
|
verbose: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(staticFileMiddleware);
|
app.use(staticFileMiddleware);
|
||||||
|
|
||||||
const listener = app.listen(fe_port, fe_host, () => {
|
const listener = app.listen(fe_port, fe_host, () => {
|
||||||
const { address, port } = listener.address();
|
const { address: fe_address, port: fe_port } =
|
||||||
$.info(`[FRONTEND] ${address}:${port}`);
|
listener.address();
|
||||||
|
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
|
||||||
|
if (fe_be_path) {
|
||||||
|
$.info(
|
||||||
|
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
|
||||||
|
);
|
||||||
|
$.info(
|
||||||
|
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
|
||||||
|
);
|
||||||
|
$.info(
|
||||||
|
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (data_url) {
|
||||||
|
$.info(`[BACKEND] downloading data from ${data_url}`);
|
||||||
|
download(data_url)
|
||||||
|
.then((content) => {
|
||||||
|
$.write(content, '#sub-store');
|
||||||
|
|
||||||
|
$.cache = JSON.parse(content);
|
||||||
|
$.persistCache();
|
||||||
|
|
||||||
|
migrate();
|
||||||
|
$.info(`[BACKEND] restored data from ${data_url}`);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
$.error(`[BACKEND] restore data failed`);
|
||||||
|
console.error(e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { ENV } from '@/vendor/open-api';
|
import { ENV } from '@/vendor/open-api';
|
||||||
import { failed, success } from '@/restful/response';
|
import { failed, success } from '@/restful/response';
|
||||||
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
|
import { updateArtifactStore, updateAvatar } from '@/restful/settings';
|
||||||
import resourceCache from '@/utils/resource-cache';
|
import resourceCache from '@/utils/resource-cache';
|
||||||
|
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||||
|
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||||
import {
|
import {
|
||||||
GIST_BACKUP_FILE_NAME,
|
GIST_BACKUP_FILE_NAME,
|
||||||
GIST_BACKUP_KEY,
|
GIST_BACKUP_KEY,
|
||||||
@@ -25,7 +27,18 @@ export default function register($app) {
|
|||||||
res.set('content-type', 'application/json')
|
res.set('content-type', 'application/json')
|
||||||
.set(
|
.set(
|
||||||
'content-disposition',
|
'content-disposition',
|
||||||
'attachment; filename="sub-store.json"',
|
`attachment; filename="${encodeURIComponent(
|
||||||
|
`sub-store_data_${new Date()
|
||||||
|
.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
})
|
||||||
|
.replace(/\D/g, '')}.json`,
|
||||||
|
)}"`,
|
||||||
)
|
)
|
||||||
.send(
|
.send(
|
||||||
$.env.isNode
|
$.env.isNode
|
||||||
@@ -63,19 +76,101 @@ export default function register($app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEnv(req, res) {
|
function getEnv(req, res) {
|
||||||
|
if (req.query.share) {
|
||||||
|
env.feature.share = true;
|
||||||
|
}
|
||||||
success(res, env);
|
success(res, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh(_, res) {
|
async function refresh(_, res) {
|
||||||
// 1. get GitHub avatar and artifact store
|
// 1. get GitHub avatar and artifact store
|
||||||
await updateGitHubAvatar();
|
await updateAvatar();
|
||||||
await updateArtifactStore();
|
await updateArtifactStore();
|
||||||
|
|
||||||
// 2. clear resource cache
|
// 2. clear resource cache
|
||||||
resourceCache.revokeAll();
|
resourceCache.revokeAll();
|
||||||
|
scriptResourceCache.revokeAll();
|
||||||
|
headersResourceCache.revokeAll();
|
||||||
success(res);
|
success(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function gistBackupAction(action) {
|
||||||
|
// read token
|
||||||
|
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||||
|
if (!gistToken) throw new Error('GitHub Token is required for backup!');
|
||||||
|
|
||||||
|
const gist = new Gist({
|
||||||
|
token: gistToken,
|
||||||
|
key: GIST_BACKUP_KEY,
|
||||||
|
syncPlatform,
|
||||||
|
});
|
||||||
|
let content;
|
||||||
|
const settings = $.read(SETTINGS_KEY);
|
||||||
|
const updated = settings.syncTime;
|
||||||
|
switch (action) {
|
||||||
|
case 'upload':
|
||||||
|
try {
|
||||||
|
content = $.read('#sub-store');
|
||||||
|
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
|
||||||
|
$.info(`下载备份, 与本地内容对比...`);
|
||||||
|
const onlineContent = await gist.download(
|
||||||
|
GIST_BACKUP_FILE_NAME,
|
||||||
|
);
|
||||||
|
if (onlineContent === content) {
|
||||||
|
$.info(`内容一致, 无需上传备份`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
$.error(`${error.message ?? error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 },
|
||||||
|
});
|
||||||
|
$.info(`上传备份完成`);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
async function gistBackup(req, res) {
|
async function gistBackup(req, res) {
|
||||||
const { action } = req.query;
|
const { action } = req.query;
|
||||||
// read token
|
// read token
|
||||||
@@ -89,77 +184,23 @@ async function gistBackup(req, res) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const gist = new Gist({
|
|
||||||
token: gistToken,
|
|
||||||
key: GIST_BACKUP_KEY,
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
let content;
|
await gistBackupAction(action);
|
||||||
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);
|
success(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Failed to ${action} gist data.\nReason: ${err.message ?? err}`,
|
||||||
|
);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
'BACKUP_FAILED',
|
'BACKUP_FAILED',
|
||||||
`Failed to ${action} data to gist!`,
|
`Failed to ${action} gist data!`,
|
||||||
`Reason: ${err.message ?? err}`,
|
`Reason: ${err.message ?? err}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { gistBackupAction };
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ function getModule(req, res) {
|
|||||||
const allModules = $.read(MODULES_KEY);
|
const allModules = $.read(MODULES_KEY);
|
||||||
const module = findByName(allModules, name);
|
const module = findByName(allModules, name);
|
||||||
if (module) {
|
if (module) {
|
||||||
res.status(200).json(module.content);
|
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||||
|
module.content,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ async function getNodeInfo(req, res) {
|
|||||||
const info = await $http
|
const info = await $http
|
||||||
.get({
|
.get({
|
||||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||||
proxy.server,
|
`${proxy.server}`
|
||||||
|
.trim()
|
||||||
|
.replace(/^\[/, '')
|
||||||
|
.replace(/\]$/, ''),
|
||||||
)}?lang=${lang}`,
|
)}?lang=${lang}`,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
|
|||||||
54
backend/src/restful/parser.js
Normal file
54
backend/src/restful/parser.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { success, failed } from '@/restful/response';
|
||||||
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
|
import { RuleUtils } from '@/core/rule-utils';
|
||||||
|
|
||||||
|
export default function register($app) {
|
||||||
|
$app.route('/api/proxy/parse').post(proxy_parser);
|
||||||
|
$app.route('/api/rule/parse').post(rule_parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 感谢 izhangxm 的 PR!
|
||||||
|
* 目前没有节点操作, 没有支持完整参数, 以后再完善一下
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***
|
||||||
|
* 代理服务器协议转换接口。
|
||||||
|
* 请求方法为POST,数据为json。需要提供data和client字段。
|
||||||
|
* data: string, 协议数据,每行一个或者是clash
|
||||||
|
* client: string, 目标平台名称,见backend/src/core/proxy-utils/producers/index.js
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function proxy_parser(req, res) {
|
||||||
|
const { data, client, content, platform } = req.body;
|
||||||
|
var result = {};
|
||||||
|
try {
|
||||||
|
var proxies = ProxyUtils.parse(data ?? content);
|
||||||
|
var par_res = ProxyUtils.produce(proxies, client ?? platform);
|
||||||
|
result['par_res'] = par_res;
|
||||||
|
} catch (err) {
|
||||||
|
failed(res, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
success(res, result);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 规则转换接口。
|
||||||
|
* 请求方法为POST,数据为json。需要提供data和client字段。
|
||||||
|
* data: string, 多行规则字符串
|
||||||
|
* client: string, 目标平台名称,具体见backend/src/core/rule-utils/producers.js
|
||||||
|
*/
|
||||||
|
function rule_parser(req, res) {
|
||||||
|
const { data, client, content, platform } = req.body;
|
||||||
|
var result = {};
|
||||||
|
try {
|
||||||
|
const rules = RuleUtils.parse(data ?? content);
|
||||||
|
var par_res = RuleUtils.produce(rules, client ?? platform);
|
||||||
|
result['par_res'] = par_res;
|
||||||
|
} catch (err) {
|
||||||
|
failed(res, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
success(res, result);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InternalServerError, NetworkError } from './errors';
|
import { InternalServerError } from './errors';
|
||||||
import { ProxyUtils } from '@/core/proxy-utils';
|
import { ProxyUtils } from '@/core/proxy-utils';
|
||||||
import { findByName } from '@/utils/database';
|
import { findByName } from '@/utils/database';
|
||||||
import { success, failed } from './response';
|
import { success, failed } from './response';
|
||||||
@@ -9,6 +9,87 @@ import $ from '@/core/app';
|
|||||||
export default function register($app) {
|
export default function register($app) {
|
||||||
$app.post('/api/preview/sub', compareSub);
|
$app.post('/api/preview/sub', compareSub);
|
||||||
$app.post('/api/preview/collection', compareCollection);
|
$app.post('/api/preview/collection', compareCollection);
|
||||||
|
$app.post('/api/preview/file', previewFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewFile(req, res) {
|
||||||
|
try {
|
||||||
|
const file = req.body;
|
||||||
|
let content = '';
|
||||||
|
if (file.type !== 'mihomoProfile') {
|
||||||
|
if (
|
||||||
|
file.source === 'local' &&
|
||||||
|
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
|
||||||
|
) {
|
||||||
|
content = file.content;
|
||||||
|
} else {
|
||||||
|
const errors = {};
|
||||||
|
content = await Promise.all(
|
||||||
|
file.url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)
|
||||||
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(url, file.ua);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!file.ignoreFailedRemoteFile &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.mergeSources === 'localFirst') {
|
||||||
|
content.unshift(file.content);
|
||||||
|
} else if (file.mergeSources === 'remoteFirst') {
|
||||||
|
content.push(file.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// parse proxies
|
||||||
|
const files = (Array.isArray(content) ? content : [content]).flat();
|
||||||
|
let filesContent = files
|
||||||
|
.filter((i) => i != null && i !== '')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
const processed =
|
||||||
|
Array.isArray(file.process) && file.process.length > 0
|
||||||
|
? await ProxyUtils.process(
|
||||||
|
{ $files: files, $content: filesContent, $file: file },
|
||||||
|
file.process,
|
||||||
|
)
|
||||||
|
: { $content: filesContent, $files: files };
|
||||||
|
|
||||||
|
// produce
|
||||||
|
success(res, {
|
||||||
|
original: filesContent,
|
||||||
|
processed: processed?.$content ?? '',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
$.error(err.message ?? err);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
`INTERNAL_SERVER_ERROR`,
|
||||||
|
`Failed to preview file`,
|
||||||
|
`Reason: ${err.message ?? err}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compareSub(req, res) {
|
async function compareSub(req, res) {
|
||||||
@@ -22,24 +103,40 @@ async function compareSub(req, res) {
|
|||||||
) {
|
) {
|
||||||
content = sub.content;
|
content = sub.content;
|
||||||
} else {
|
} else {
|
||||||
try {
|
const errors = {};
|
||||||
content = await Promise.all(
|
content = await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(
|
||||||
|
url,
|
||||||
|
sub.ua,
|
||||||
|
undefined,
|
||||||
|
sub.proxy,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
failed(
|
|
||||||
res,
|
|
||||||
new NetworkError(
|
|
||||||
'FAILED_TO_DOWNLOAD_RESOURCE',
|
|
||||||
'无法下载远程资源',
|
|
||||||
`Reason: ${err}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (sub.mergeSources === 'localFirst') {
|
if (sub.mergeSources === 'localFirst') {
|
||||||
content.unshift(sub.content);
|
content.unshift(sub.content);
|
||||||
@@ -55,7 +152,8 @@ async function compareSub(req, res) {
|
|||||||
// add id
|
// add id
|
||||||
original.forEach((proxy, i) => {
|
original.forEach((proxy, i) => {
|
||||||
proxy.id = i;
|
proxy.id = i;
|
||||||
proxy.subName = sub.name;
|
proxy._subName = sub.name;
|
||||||
|
proxy._subDisplayName = sub.displayName;
|
||||||
});
|
});
|
||||||
|
|
||||||
// apply processors
|
// apply processors
|
||||||
@@ -85,71 +183,117 @@ async function compareCollection(req, res) {
|
|||||||
try {
|
try {
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const collection = req.body;
|
const collection = req.body;
|
||||||
const subnames = collection.subscriptions;
|
const subnames = [...collection.subscriptions];
|
||||||
|
let subscriptionTags = collection.subscriptionTags;
|
||||||
|
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
|
||||||
|
allSubs.forEach((sub) => {
|
||||||
|
if (
|
||||||
|
Array.isArray(sub.tag) &&
|
||||||
|
sub.tag.length > 0 &&
|
||||||
|
!subnames.includes(sub.name) &&
|
||||||
|
sub.tag.some((tag) => subscriptionTags.includes(tag))
|
||||||
|
) {
|
||||||
|
subnames.push(sub.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
const results = {};
|
const results = {};
|
||||||
let hasError;
|
const errors = {};
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
subnames.map(async (name) => {
|
subnames.map(async (name) => {
|
||||||
if (!hasError) {
|
const sub = findByName(allSubs, name);
|
||||||
const sub = findByName(allSubs, name);
|
try {
|
||||||
try {
|
let raw;
|
||||||
let raw;
|
if (
|
||||||
if (
|
sub.source === 'local' &&
|
||||||
sub.source === 'local' &&
|
!['localFirst', 'remoteFirst'].includes(
|
||||||
!['localFirst', 'remoteFirst'].includes(
|
sub.mergeSources,
|
||||||
sub.mergeSources,
|
)
|
||||||
)
|
) {
|
||||||
) {
|
raw = sub.content;
|
||||||
raw = sub.content;
|
} else {
|
||||||
} else {
|
const errors = {};
|
||||||
raw = await Promise.all(
|
raw = await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
);
|
try {
|
||||||
if (sub.mergeSources === 'localFirst') {
|
return await download(
|
||||||
raw.unshift(sub.content);
|
url,
|
||||||
} else if (sub.mergeSources === 'remoteFirst') {
|
sub.ua,
|
||||||
raw.push(sub.content);
|
undefined,
|
||||||
}
|
sub.proxy,
|
||||||
}
|
undefined,
|
||||||
// parse proxies
|
undefined,
|
||||||
let currentProxies = (Array.isArray(raw) ? raw : [raw])
|
undefined,
|
||||||
.map((i) => ProxyUtils.parse(i))
|
true,
|
||||||
.flat();
|
);
|
||||||
|
} catch (err) {
|
||||||
currentProxies.forEach((proxy) => {
|
errors[url] = err;
|
||||||
proxy.subName = sub.name;
|
$.error(
|
||||||
proxy.collectionName = collection.name;
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
});
|
);
|
||||||
|
return '';
|
||||||
// apply processors
|
}
|
||||||
currentProxies = await ProxyUtils.process(
|
}),
|
||||||
currentProxies,
|
|
||||||
sub.process || [],
|
|
||||||
'JSON',
|
|
||||||
{ [sub.name]: sub, _collection: collection },
|
|
||||||
);
|
);
|
||||||
results[name] = currentProxies;
|
if (
|
||||||
} catch (err) {
|
!sub.ignoreFailedRemoteSub &&
|
||||||
if (!hasError) {
|
Object.keys(errors).length > 0
|
||||||
hasError = true;
|
) {
|
||||||
failed(
|
throw new Error(
|
||||||
res,
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||||
new InternalServerError(
|
errors,
|
||||||
'PROCESS_FAILED',
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
`处理子订阅 ${name} 失败`,
|
|
||||||
`Reason: ${err}`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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._subDisplayName = sub.displayName;
|
||||||
|
proxy._collectionName = collection.name;
|
||||||
|
proxy._collectionDisplayName = collection.displayName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
currentProxies = await ProxyUtils.process(
|
||||||
|
currentProxies,
|
||||||
|
sub.process || [],
|
||||||
|
'JSON',
|
||||||
|
{ [sub.name]: sub, _collection: collection },
|
||||||
|
);
|
||||||
|
results[name] = currentProxies;
|
||||||
|
} catch (err) {
|
||||||
|
errors[name] = err;
|
||||||
|
|
||||||
|
$.error(
|
||||||
|
`❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name}时出现错误:${err}!`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (hasError) return;
|
if (
|
||||||
|
!collection.ignoreFailedRemoteSub &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`组合订阅 ${collection.name} 中的子订阅 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
// merge proxies with the original order
|
// merge proxies with the original order
|
||||||
const original = Array.prototype.concat.apply(
|
const original = Array.prototype.concat.apply(
|
||||||
[],
|
[],
|
||||||
@@ -158,7 +302,8 @@ async function compareCollection(req, res) {
|
|||||||
|
|
||||||
original.forEach((proxy, i) => {
|
original.forEach((proxy, i) => {
|
||||||
proxy.id = i;
|
proxy.id = i;
|
||||||
proxy.collectionName = collection.name;
|
proxy._collectionName = collection.name;
|
||||||
|
proxy._collectionDisplayName = collection.displayName;
|
||||||
});
|
});
|
||||||
|
|
||||||
const processed = await ProxyUtils.process(
|
const processed = await ProxyUtils.process(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
|
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
|
||||||
import { success } from './response';
|
import { success, failed } from './response';
|
||||||
|
import { InternalServerError } from '@/restful/errors';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import Gist from '@/utils/gist';
|
import Gist from '@/utils/gist';
|
||||||
|
|
||||||
@@ -10,51 +11,105 @@ export default function register($app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getSettings(req, res) {
|
async function getSettings(req, res) {
|
||||||
let settings = $.read(SETTINGS_KEY);
|
try {
|
||||||
if (!settings) {
|
let settings = $.read(SETTINGS_KEY);
|
||||||
settings = {};
|
if (!settings) {
|
||||||
$.write(settings, SETTINGS_KEY);
|
settings = {};
|
||||||
}
|
$.write(settings, SETTINGS_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
if (!settings.avatarUrl) await updateGitHubAvatar();
|
if (!settings.avatarUrl) await updateAvatar();
|
||||||
if (!settings.artifactStore) await updateArtifactStore();
|
if (!settings.artifactStore) await updateArtifactStore();
|
||||||
success(res, settings);
|
|
||||||
|
success(res, settings);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`Failed to get settings: ${e.message ?? e}`);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
`FAILED_TO_GET_SETTINGS`,
|
||||||
|
`Failed to get settings`,
|
||||||
|
`Reason: ${e.message ?? e}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSettings(req, res) {
|
async function updateSettings(req, res) {
|
||||||
const settings = $.read(SETTINGS_KEY);
|
try {
|
||||||
const newSettings = {
|
const settings = $.read(SETTINGS_KEY);
|
||||||
...settings,
|
const newSettings = {
|
||||||
...req.body,
|
...settings,
|
||||||
};
|
...req.body,
|
||||||
$.write(newSettings, SETTINGS_KEY);
|
};
|
||||||
await updateGitHubAvatar();
|
$.write(newSettings, SETTINGS_KEY);
|
||||||
await updateArtifactStore();
|
await updateAvatar();
|
||||||
success(res, newSettings);
|
await updateArtifactStore();
|
||||||
|
success(res, newSettings);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`Failed to update settings: ${e.message ?? e}`);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
`FAILED_TO_UPDATE_SETTINGS`,
|
||||||
|
`Failed to update settings`,
|
||||||
|
`Reason: ${e.message ?? e}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateGitHubAvatar() {
|
export async function updateAvatar() {
|
||||||
const settings = $.read(SETTINGS_KEY);
|
const settings = $.read(SETTINGS_KEY);
|
||||||
const username = settings.githubUser;
|
const { githubUser: username, syncPlatform } = settings;
|
||||||
if (username) {
|
if (username) {
|
||||||
try {
|
if (syncPlatform === 'gitlab') {
|
||||||
const data = await $.http
|
try {
|
||||||
.get({
|
const data = await $.http
|
||||||
url: `https://api.github.com/users/${username}`,
|
.get({
|
||||||
headers: {
|
url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(
|
||||||
'User-Agent':
|
username,
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
)}`,
|
||||||
},
|
headers: {
|
||||||
})
|
'User-Agent':
|
||||||
.then((resp) => JSON.parse(resp.body));
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||||
settings.avatarUrl = data['avatar_url'];
|
},
|
||||||
$.write(settings, SETTINGS_KEY);
|
})
|
||||||
} catch (err) {
|
.then((resp) => JSON.parse(resp.body));
|
||||||
$.error(
|
settings.avatarUrl = data[0]['avatar_url'].replace(
|
||||||
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
/(\?|&)s=\d+(&|$)/,
|
||||||
err.message ?? err
|
'$1s=160$2',
|
||||||
}`,
|
);
|
||||||
);
|
$.write(settings, SETTINGS_KEY);
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Failed to fetch GitLab avatar for User: ${username}. Reason: ${
|
||||||
|
err.message ?? err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const data = await $.http
|
||||||
|
.get({
|
||||||
|
url: `https://api.github.com/users/${encodeURIComponent(
|
||||||
|
username,
|
||||||
|
)}`,
|
||||||
|
headers: {
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((resp) => JSON.parse(resp.body));
|
||||||
|
settings.avatarUrl = data['avatar_url'];
|
||||||
|
$.write(settings, SETTINGS_KEY);
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
|
||||||
|
err.message ?? err
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,25 +117,30 @@ export async function updateGitHubAvatar() {
|
|||||||
export async function updateArtifactStore() {
|
export async function updateArtifactStore() {
|
||||||
$.log('Updating artifact store');
|
$.log('Updating artifact store');
|
||||||
const settings = $.read(SETTINGS_KEY);
|
const settings = $.read(SETTINGS_KEY);
|
||||||
const { githubUser, gistToken } = settings;
|
const { gistToken, syncPlatform } = settings;
|
||||||
if (githubUser && gistToken) {
|
if (gistToken) {
|
||||||
const manager = new Gist({
|
const manager = new Gist({
|
||||||
token: gistToken,
|
token: gistToken,
|
||||||
key: ARTIFACT_REPOSITORY_KEY,
|
key: ARTIFACT_REPOSITORY_KEY,
|
||||||
|
syncPlatform,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gistId = await manager.locate();
|
const gist = await manager.locate();
|
||||||
if (gistId !== -1) {
|
const url = gist?.html_url ?? gist?.web_url;
|
||||||
settings.artifactStore = `https://gist.github.com/${githubUser}/${gistId}`;
|
if (url) {
|
||||||
$.write(settings, SETTINGS_KEY);
|
$.log(`找到 Sub-Store Gist: ${url}`);
|
||||||
|
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
|
||||||
|
settings.artifactStore = url;
|
||||||
|
settings.artifactStoreStatus = 'VALID';
|
||||||
|
} else {
|
||||||
|
$.error(`找不到 Sub-Store Gist`);
|
||||||
|
settings.artifactStoreStatus = 'NOT FOUND';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.error(
|
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
|
||||||
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
|
settings.artifactStoreStatus = 'ERROR';
|
||||||
err.message ?? err
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
$.write(settings, SETTINGS_KEY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
import {
|
||||||
|
ARTIFACTS_KEY,
|
||||||
|
COLLECTIONS_KEY,
|
||||||
|
SUBS_KEY,
|
||||||
|
FILES_KEY,
|
||||||
|
} from '@/constants';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { success } from '@/restful/response';
|
import { success } from '@/restful/response';
|
||||||
|
|
||||||
@@ -6,6 +11,7 @@ export default function register($app) {
|
|||||||
$app.post('/api/sort/subs', sortSubs);
|
$app.post('/api/sort/subs', sortSubs);
|
||||||
$app.post('/api/sort/collections', sortCollections);
|
$app.post('/api/sort/collections', sortCollections);
|
||||||
$app.post('/api/sort/artifacts', sortArtifacts);
|
$app.post('/api/sort/artifacts', sortArtifacts);
|
||||||
|
$app.post('/api/sort/files', sortFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortSubs(req, res) {
|
function sortSubs(req, res) {
|
||||||
@@ -33,3 +39,11 @@ function sortArtifacts(req, res) {
|
|||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
success(res, allArtifacts);
|
success(res, allArtifacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortFiles(req, res) {
|
||||||
|
const orders = req.body;
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||||
|
$.write(allFiles, FILES_KEY);
|
||||||
|
success(res, allFiles);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import {
|
|||||||
} from './errors';
|
} from './errors';
|
||||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||||
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||||
import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
|
import {
|
||||||
|
getFlowHeaders,
|
||||||
|
parseFlowHeaders,
|
||||||
|
getRmainingDays,
|
||||||
|
} from '@/utils/flow';
|
||||||
import { success, failed } from './response';
|
import { success, failed } from './response';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
|
||||||
@@ -30,6 +34,11 @@ export default function register($app) {
|
|||||||
async function getFlowInfo(req, res) {
|
async function getFlowInfo(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
let { url } = req.query;
|
||||||
|
if (url) {
|
||||||
|
url = decodeURIComponent(url);
|
||||||
|
$.info(`指定远程订阅 URL: ${url}`);
|
||||||
|
}
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
if (!sub) {
|
if (!sub) {
|
||||||
@@ -43,20 +52,107 @@ async function getFlowInfo(req, res) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (sub.source === 'local') {
|
if (
|
||||||
failed(
|
sub.source === 'local' &&
|
||||||
res,
|
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||||
new RequestInvalidError(
|
) {
|
||||||
'NO_FLOW_INFO',
|
if (sub.subUserinfo) {
|
||||||
'N/A',
|
let subUserInfo;
|
||||||
`Local subscription ${name} has no flow information!`,
|
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||||
),
|
try {
|
||||||
);
|
subUserInfo = await getFlowHeaders(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
sub.proxy,
|
||||||
|
sub.subUserinfo,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`订阅 ${name} 使用自定义流量链接 ${
|
||||||
|
sub.subUserinfo
|
||||||
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subUserInfo = sub.subUserinfo;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
success(res, {
|
||||||
|
...parseFlowHeaders(subUserInfo),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`Failed to parse flow info for local subscription ${name}: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'NO_FLOW_INFO',
|
||||||
|
'N/A',
|
||||||
|
`Failed to parse flow info`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'NO_FLOW_INFO',
|
||||||
|
'N/A',
|
||||||
|
`Local subscription ${name} has no flow information!`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const flowHeaders = await getFlowHeaders(sub.url);
|
url =
|
||||||
if (!flowHeaders) {
|
`${url || sub.url}`
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)?.[0] || '';
|
||||||
|
|
||||||
|
let $arguments = {};
|
||||||
|
const rawArgs = url.split('#');
|
||||||
|
url = url.split('#')[0];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($arguments.noFlow) {
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'NO_FLOW_INFO',
|
||||||
|
'N/A',
|
||||||
|
`Subscription ${name}: noFlow`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const flowHeaders = await getFlowHeaders(
|
||||||
|
$arguments?.insecure ? `${url}#insecure` : url,
|
||||||
|
$arguments.flowUserAgent,
|
||||||
|
undefined,
|
||||||
|
sub.proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
|
);
|
||||||
|
if (!flowHeaders && !sub.subUserinfo) {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
@@ -67,8 +163,56 @@ async function getFlowInfo(req, res) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
success(res, parseFlowHeaders(flowHeaders));
|
const remainingDays = getRmainingDays({
|
||||||
|
resetDay: $arguments.resetDay,
|
||||||
|
startDate: $arguments.startDate,
|
||||||
|
cycleDays: $arguments.cycleDays,
|
||||||
|
});
|
||||||
|
let subUserInfo;
|
||||||
|
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
||||||
|
try {
|
||||||
|
subUserInfo = await getFlowHeaders(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
sub.proxy,
|
||||||
|
sub.subUserinfo,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`订阅 ${name} 使用自定义流量链接 ${
|
||||||
|
sub.subUserinfo
|
||||||
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subUserInfo = sub.subUserinfo;
|
||||||
|
}
|
||||||
|
const result = {
|
||||||
|
...parseFlowHeaders(
|
||||||
|
[subUserInfo, flowHeaders].filter((i) => i).join('; '),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (remainingDays != null) {
|
||||||
|
result.remainingDays = remainingDays;
|
||||||
|
}
|
||||||
|
success(res, result);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`Failed to parse flow info for local subscription ${name}: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'NO_FLOW_INFO',
|
||||||
|
'N/A',
|
||||||
|
`Failed to parse flow info`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
@@ -111,11 +255,32 @@ function createSubscription(req, res) {
|
|||||||
|
|
||||||
function getSubscription(req, res) {
|
function getSubscription(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
|
let { raw } = req.query;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
if (sub) {
|
if (sub) {
|
||||||
success(res, sub);
|
if (raw) {
|
||||||
|
res.set('content-type', 'application/json')
|
||||||
|
.set(
|
||||||
|
'content-disposition',
|
||||||
|
`attachment; filename="${encodeURIComponent(
|
||||||
|
`sub-store_subscription_${name}_${new Date()
|
||||||
|
.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
})
|
||||||
|
.replace(/\D/g, '')}.json`,
|
||||||
|
)}"`,
|
||||||
|
)
|
||||||
|
.send(JSON.stringify(sub));
|
||||||
|
} else {
|
||||||
|
success(res, sub);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
COLLECTIONS_KEY,
|
COLLECTIONS_KEY,
|
||||||
RULES_KEY,
|
RULES_KEY,
|
||||||
SUBS_KEY,
|
SUBS_KEY,
|
||||||
|
FILES_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { failed, success } from '@/restful/response';
|
import { failed, success } from '@/restful/response';
|
||||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||||
@@ -30,23 +31,71 @@ async function produceArtifact({
|
|||||||
ua,
|
ua,
|
||||||
content,
|
content,
|
||||||
mergeSources,
|
mergeSources,
|
||||||
|
ignoreFailedRemoteSub,
|
||||||
|
ignoreFailedRemoteFile,
|
||||||
|
produceType,
|
||||||
|
produceOpts = {},
|
||||||
|
subscription,
|
||||||
|
awaitCustomCache,
|
||||||
|
$options,
|
||||||
|
proxy,
|
||||||
|
noCache,
|
||||||
}) {
|
}) {
|
||||||
platform = platform || 'JSON';
|
platform = platform || 'JSON';
|
||||||
|
|
||||||
if (type === 'subscription') {
|
if (['subscription', 'sub'].includes(type)) {
|
||||||
const allSubs = $.read(SUBS_KEY);
|
let sub;
|
||||||
const sub = findByName(allSubs, name);
|
if (name) {
|
||||||
|
const allSubs = $.read(SUBS_KEY);
|
||||||
|
sub = findByName(allSubs, name);
|
||||||
|
if (!sub) throw new Error(`找不到订阅 ${name}`);
|
||||||
|
} else if (subscription) {
|
||||||
|
sub = subscription;
|
||||||
|
} else {
|
||||||
|
throw new Error('未提供订阅名称或订阅数据');
|
||||||
|
}
|
||||||
let raw;
|
let raw;
|
||||||
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
|
||||||
raw = content;
|
raw = content;
|
||||||
} else if (url) {
|
} else if (url) {
|
||||||
|
const errors = {};
|
||||||
raw = await Promise.all(
|
raw = await Promise.all(
|
||||||
url
|
url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(
|
||||||
|
url,
|
||||||
|
ua || sub.ua,
|
||||||
|
undefined,
|
||||||
|
proxy || sub.proxy,
|
||||||
|
undefined,
|
||||||
|
awaitCustomCache,
|
||||||
|
noCache || sub.noCache,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||||
|
}
|
||||||
|
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (mergeSources === 'localFirst') {
|
if (mergeSources === 'localFirst') {
|
||||||
raw.unshift(content);
|
raw.unshift(content);
|
||||||
} else if (mergeSources === 'remoteFirst') {
|
} else if (mergeSources === 'remoteFirst') {
|
||||||
@@ -58,13 +107,44 @@ async function produceArtifact({
|
|||||||
) {
|
) {
|
||||||
raw = sub.content;
|
raw = sub.content;
|
||||||
} else {
|
} else {
|
||||||
|
const errors = {};
|
||||||
raw = await Promise.all(
|
raw = await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(
|
||||||
|
url,
|
||||||
|
ua || sub.ua,
|
||||||
|
undefined,
|
||||||
|
proxy || sub.proxy,
|
||||||
|
undefined,
|
||||||
|
awaitCustomCache,
|
||||||
|
noCache || sub.noCache,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
|
||||||
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||||
|
}
|
||||||
|
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||||
|
', ',
|
||||||
|
)} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (sub.mergeSources === 'localFirst') {
|
if (sub.mergeSources === 'localFirst') {
|
||||||
raw.unshift(sub.content);
|
raw.unshift(sub.content);
|
||||||
} else if (sub.mergeSources === 'remoteFirst') {
|
} else if (sub.mergeSources === 'remoteFirst') {
|
||||||
@@ -77,7 +157,8 @@ async function produceArtifact({
|
|||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
proxies.forEach((proxy) => {
|
proxies.forEach((proxy) => {
|
||||||
proxy.subName = sub.name;
|
proxy._subName = sub.name;
|
||||||
|
proxy._subDisplayName = sub.displayName;
|
||||||
});
|
});
|
||||||
// apply processors
|
// apply processors
|
||||||
proxies = await ProxyUtils.process(
|
proxies = await ProxyUtils.process(
|
||||||
@@ -85,6 +166,7 @@ async function produceArtifact({
|
|||||||
sub.process || [],
|
sub.process || [],
|
||||||
platform,
|
platform,
|
||||||
{ [sub.name]: sub },
|
{ [sub.name]: sub },
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
if (proxies.length === 0) {
|
if (proxies.length === 0) {
|
||||||
throw new Error(`订阅 ${name} 中不含有效节点`);
|
throw new Error(`订阅 ${name} 中不含有效节点`);
|
||||||
@@ -107,12 +189,26 @@ async function produceArtifact({
|
|||||||
exist[proxy.name] = true;
|
exist[proxy.name] = true;
|
||||||
}
|
}
|
||||||
// produce
|
// produce
|
||||||
return ProxyUtils.produce(proxies, platform);
|
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||||
} else if (type === 'collection') {
|
} else if (['collection', 'col'].includes(type)) {
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
const collection = findByName(allCols, name);
|
const collection = findByName(allCols, name);
|
||||||
const subnames = collection.subscriptions;
|
if (!collection) throw new Error(`找不到组合订阅 ${name}`);
|
||||||
|
const subnames = [...collection.subscriptions];
|
||||||
|
let subscriptionTags = collection.subscriptionTags;
|
||||||
|
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
|
||||||
|
allSubs.forEach((sub) => {
|
||||||
|
if (
|
||||||
|
Array.isArray(sub.tag) &&
|
||||||
|
sub.tag.length > 0 &&
|
||||||
|
!subnames.includes(sub.name) &&
|
||||||
|
sub.tag.some((tag) => subscriptionTags.includes(tag))
|
||||||
|
) {
|
||||||
|
subnames.push(sub.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
const results = {};
|
const results = {};
|
||||||
const errors = {};
|
const errors = {};
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
@@ -120,6 +216,14 @@ async function produceArtifact({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
subnames.map(async (name) => {
|
subnames.map(async (name) => {
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
|
const passThroughUA = sub.passThroughUA;
|
||||||
|
let reqUA = sub.ua;
|
||||||
|
if (passThroughUA) {
|
||||||
|
$.info(
|
||||||
|
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,
|
||||||
|
);
|
||||||
|
reqUA = ua;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
$.info(`正在处理子订阅:${sub.name}...`);
|
$.info(`正在处理子订阅:${sub.name}...`);
|
||||||
let raw;
|
let raw;
|
||||||
@@ -131,13 +235,45 @@ async function produceArtifact({
|
|||||||
) {
|
) {
|
||||||
raw = sub.content;
|
raw = sub.content;
|
||||||
} else {
|
} else {
|
||||||
|
const errors = {};
|
||||||
raw = await await Promise.all(
|
raw = await await Promise.all(
|
||||||
sub.url
|
sub.url
|
||||||
.split(/[\r\n]+/)
|
.split(/[\r\n]+/)
|
||||||
.map((i) => i.trim())
|
.map((i) => i.trim())
|
||||||
.filter((i) => i.length)
|
.filter((i) => i.length)
|
||||||
.map((url) => download(url, sub.ua)),
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(
|
||||||
|
url,
|
||||||
|
reqUA,
|
||||||
|
undefined,
|
||||||
|
proxy ||
|
||||||
|
sub.proxy ||
|
||||||
|
collection.proxy,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
noCache || sub.noCache,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
!sub.ignoreFailedRemoteSub &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (sub.mergeSources === 'localFirst') {
|
if (sub.mergeSources === 'localFirst') {
|
||||||
raw.unshift(sub.content);
|
raw.unshift(sub.content);
|
||||||
} else if (sub.mergeSources === 'remoteFirst') {
|
} else if (sub.mergeSources === 'remoteFirst') {
|
||||||
@@ -150,8 +286,10 @@ async function produceArtifact({
|
|||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
currentProxies.forEach((proxy) => {
|
currentProxies.forEach((proxy) => {
|
||||||
proxy.subName = sub.name;
|
proxy._subName = sub.name;
|
||||||
proxy.collectionName = collection.name;
|
proxy._subDisplayName = sub.displayName;
|
||||||
|
proxy._collectionName = collection.name;
|
||||||
|
proxy._collectionDisplayName = collection.displayName;
|
||||||
});
|
});
|
||||||
|
|
||||||
// apply processors
|
// apply processors
|
||||||
@@ -159,7 +297,11 @@ async function produceArtifact({
|
|||||||
currentProxies,
|
currentProxies,
|
||||||
sub.process || [],
|
sub.process || [],
|
||||||
platform,
|
platform,
|
||||||
{ [sub.name]: sub, _collection: collection },
|
{
|
||||||
|
[sub.name]: sub,
|
||||||
|
_collection: collection,
|
||||||
|
$options,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
results[name] = currentProxies;
|
results[name] = currentProxies;
|
||||||
processed++;
|
processed++;
|
||||||
@@ -174,15 +316,21 @@ async function produceArtifact({
|
|||||||
$.error(
|
$.error(
|
||||||
`❌ 处理组合订阅中的子订阅: ${
|
`❌ 处理组合订阅中的子订阅: ${
|
||||||
sub.name
|
sub.name
|
||||||
}时出现错误:${err},该订阅已被跳过!进度--${
|
}时出现错误:${err}!进度--${
|
||||||
100 * (processed / subnames.length).toFixed(1)
|
100 * (processed / subnames.length).toFixed(1)
|
||||||
}%`,
|
}%`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
|
||||||
if (Object.keys(errors).length > 0) {
|
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||||
|
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!collectionIgnoreFailedRemoteSub &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
|
||||||
', ',
|
', ',
|
||||||
@@ -197,7 +345,8 @@ async function produceArtifact({
|
|||||||
);
|
);
|
||||||
|
|
||||||
proxies.forEach((proxy) => {
|
proxies.forEach((proxy) => {
|
||||||
proxy.collectionName = collection.name;
|
proxy._collectionName = collection.name;
|
||||||
|
proxy._collectionDisplayName = collection.displayName;
|
||||||
});
|
});
|
||||||
|
|
||||||
// apply own processors
|
// apply own processors
|
||||||
@@ -206,6 +355,7 @@ async function produceArtifact({
|
|||||||
collection.process || [],
|
collection.process || [],
|
||||||
platform,
|
platform,
|
||||||
{ _collection: collection },
|
{ _collection: collection },
|
||||||
|
$options,
|
||||||
);
|
);
|
||||||
if (proxies.length === 0) {
|
if (proxies.length === 0) {
|
||||||
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
||||||
@@ -227,10 +377,11 @@ async function produceArtifact({
|
|||||||
}
|
}
|
||||||
exist[proxy.name] = true;
|
exist[proxy.name] = true;
|
||||||
}
|
}
|
||||||
return ProxyUtils.produce(proxies, platform);
|
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||||
} else if (type === 'rule') {
|
} else if (type === 'rule') {
|
||||||
const allRules = $.read(RULES_KEY);
|
const allRules = $.read(RULES_KEY);
|
||||||
const rule = findByName(allRules, name);
|
const rule = findByName(allRules, name);
|
||||||
|
if (!rule) throw new Error(`找不到规则 ${name}`);
|
||||||
let rules = [];
|
let rules = [];
|
||||||
for (let i = 0; i < rule.urls.length; i++) {
|
for (let i = 0; i < rule.urls.length; i++) {
|
||||||
const url = rule.urls[i];
|
const url = rule.urls[i];
|
||||||
@@ -255,74 +406,354 @@ async function produceArtifact({
|
|||||||
]);
|
]);
|
||||||
// produce output
|
// produce output
|
||||||
return RuleUtils.produce(rules, platform);
|
return RuleUtils.produce(rules, platform);
|
||||||
|
} else if (type === 'file') {
|
||||||
|
const allFiles = $.read(FILES_KEY);
|
||||||
|
const file = findByName(allFiles, name);
|
||||||
|
if (!file) throw new Error(`找不到文件 ${name}`);
|
||||||
|
let raw = '';
|
||||||
|
console.log(file);
|
||||||
|
if (file.type !== 'mihomoProfile') {
|
||||||
|
if (
|
||||||
|
content &&
|
||||||
|
!['localFirst', 'remoteFirst'].includes(mergeSources)
|
||||||
|
) {
|
||||||
|
raw = content;
|
||||||
|
} else if (url) {
|
||||||
|
const errors = {};
|
||||||
|
raw = await Promise.all(
|
||||||
|
url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)
|
||||||
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(
|
||||||
|
url,
|
||||||
|
ua || file.ua,
|
||||||
|
undefined,
|
||||||
|
file.proxy || proxy,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
noCache,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
|
||||||
|
if (
|
||||||
|
ignoreFailedRemoteFile != null &&
|
||||||
|
ignoreFailedRemoteFile !== ''
|
||||||
|
) {
|
||||||
|
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!fileIgnoreFailedRemoteFile &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mergeSources === 'localFirst') {
|
||||||
|
raw.unshift(content);
|
||||||
|
} else if (mergeSources === 'remoteFirst') {
|
||||||
|
raw.push(content);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
file.source === 'local' &&
|
||||||
|
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
|
||||||
|
) {
|
||||||
|
raw = file.content;
|
||||||
|
} else {
|
||||||
|
const errors = {};
|
||||||
|
raw = await Promise.all(
|
||||||
|
file.url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)
|
||||||
|
.map(async (url) => {
|
||||||
|
try {
|
||||||
|
return await download(
|
||||||
|
url,
|
||||||
|
ua || file.ua,
|
||||||
|
undefined,
|
||||||
|
file.proxy || proxy,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
noCache,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errors[url] = err;
|
||||||
|
$.error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||||
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
|
||||||
|
if (
|
||||||
|
ignoreFailedRemoteFile != null &&
|
||||||
|
ignoreFailedRemoteFile !== ''
|
||||||
|
) {
|
||||||
|
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!fileIgnoreFailedRemoteFile &&
|
||||||
|
Object.keys(errors).length > 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`文件 ${file.name} 的远程文件 ${Object.keys(
|
||||||
|
errors,
|
||||||
|
).join(', ')} 发生错误, 请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file.mergeSources === 'localFirst') {
|
||||||
|
raw.unshift(file.content);
|
||||||
|
} else if (file.mergeSources === 'remoteFirst') {
|
||||||
|
raw.push(file.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
||||||
|
let filesContent = files
|
||||||
|
.filter((i) => i != null && i !== '')
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// apply processors
|
||||||
|
const processed =
|
||||||
|
Array.isArray(file.process) && file.process.length > 0
|
||||||
|
? await ProxyUtils.process(
|
||||||
|
{
|
||||||
|
$files: files,
|
||||||
|
$content: filesContent,
|
||||||
|
$options,
|
||||||
|
$file: file,
|
||||||
|
},
|
||||||
|
file.process,
|
||||||
|
)
|
||||||
|
: { $content: filesContent, $files: files, $options };
|
||||||
|
|
||||||
|
return processed?.$content ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncAllArtifacts(_, res) {
|
async function syncArtifacts() {
|
||||||
$.info('开始同步所有远程配置...');
|
$.info('开始同步所有远程配置...');
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const files = {};
|
const files = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const valid = [];
|
||||||
|
const invalid = [];
|
||||||
|
const allSubs = $.read(SUBS_KEY);
|
||||||
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
|
const subNames = [];
|
||||||
|
allArtifacts.map((artifact) => {
|
||||||
|
if (artifact.sync && artifact.source) {
|
||||||
|
if (artifact.type === 'subscription') {
|
||||||
|
const subName = artifact.source;
|
||||||
|
const sub = findByName(allSubs, subName);
|
||||||
|
if (sub && sub.url && !subNames.includes(subName)) {
|
||||||
|
subNames.push(subName);
|
||||||
|
}
|
||||||
|
} else if (artifact.type === 'collection') {
|
||||||
|
const collection = findByName(allCols, artifact.source);
|
||||||
|
if (collection && Array.isArray(collection.subscriptions)) {
|
||||||
|
collection.subscriptions.map((subName) => {
|
||||||
|
const sub = findByName(allSubs, subName);
|
||||||
|
if (sub && sub.url && !subNames.includes(subName)) {
|
||||||
|
subNames.push(subName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subNames.length > 0) {
|
||||||
|
await Promise.all(
|
||||||
|
subNames.map(async (subName) => {
|
||||||
|
try {
|
||||||
|
await produceArtifact({
|
||||||
|
type: 'subscription',
|
||||||
|
name: subName,
|
||||||
|
awaitCustomCache: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// $.error(`${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
allArtifacts.map(async (artifact) => {
|
allArtifacts.map(async (artifact) => {
|
||||||
if (artifact.sync) {
|
try {
|
||||||
$.info(`正在同步云配置:${artifact.name}...`);
|
if (artifact.sync && artifact.source) {
|
||||||
const output = await produceArtifact({
|
$.info(`正在同步云配置:${artifact.name}...`);
|
||||||
type: artifact.type,
|
|
||||||
name: artifact.source,
|
|
||||||
platform: artifact.platform,
|
|
||||||
});
|
|
||||||
|
|
||||||
files[artifact.name] = {
|
const useMihomoExternal =
|
||||||
content: output,
|
artifact.platform === 'SurgeMac';
|
||||||
};
|
|
||||||
|
if (useMihomoExternal) {
|
||||||
|
$.info(
|
||||||
|
`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = await produceArtifact({
|
||||||
|
type: artifact.type,
|
||||||
|
name: artifact.source,
|
||||||
|
platform: artifact.platform,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy':
|
||||||
|
artifact.includeUnsupportedProxy,
|
||||||
|
useMihomoExternal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (!output || output.length === 0)
|
||||||
|
// throw new Error('该配置的结果为空 不进行上传');
|
||||||
|
|
||||||
|
files[encodeURIComponent(artifact.name)] = {
|
||||||
|
content: output,
|
||||||
|
};
|
||||||
|
|
||||||
|
valid.push(artifact.name);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`生成同步配置 ${artifact.name} 发生错误: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
invalid.push(artifact.name);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
|
||||||
|
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
|
||||||
|
|
||||||
|
if (valid.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await syncToGist(files);
|
const resp = await syncToGist(files);
|
||||||
const body = JSON.parse(resp.body);
|
const body = JSON.parse(resp.body);
|
||||||
|
|
||||||
|
delete body.history;
|
||||||
|
delete body.forks;
|
||||||
|
delete body.owner;
|
||||||
|
Object.values(body.files).forEach((file) => {
|
||||||
|
delete file.content;
|
||||||
|
});
|
||||||
|
$.info('上传配置响应:');
|
||||||
|
$.info(JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
for (const artifact of allArtifacts) {
|
for (const artifact of allArtifacts) {
|
||||||
if (artifact.sync) {
|
if (
|
||||||
|
artifact.sync &&
|
||||||
|
artifact.source &&
|
||||||
|
valid.includes(artifact.name)
|
||||||
|
) {
|
||||||
artifact.updated = new Date().getTime();
|
artifact.updated = new Date().getTime();
|
||||||
// extract real url from gist
|
// extract real url from gist
|
||||||
artifact.url = body.files[artifact.name].raw_url.replace(
|
let files = body.files;
|
||||||
/\/raw\/[^/]*\/(.*)/,
|
let isGitLab;
|
||||||
'/raw/$1',
|
if (Array.isArray(files)) {
|
||||||
|
isGitLab = true;
|
||||||
|
files = Object.fromEntries(
|
||||||
|
files.map((item) => [item.path, item]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const raw_url =
|
||||||
|
files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||||
|
const new_url = isGitLab
|
||||||
|
? raw_url
|
||||||
|
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||||
|
$.info(
|
||||||
|
`上传配置完成\n文件列表: ${Object.keys(files).join(
|
||||||
|
', ',
|
||||||
|
)}\n当前文件: ${encodeURIComponent(
|
||||||
|
artifact.name,
|
||||||
|
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
|
||||||
);
|
);
|
||||||
|
artifact.url = new_url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
$.info('全部订阅同步成功!');
|
$.info('上传配置成功');
|
||||||
|
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$.info(`同步配置成功 ${valid.length} 个`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`同步配置失败,原因:${e.message ?? e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function syncAllArtifacts(_, res) {
|
||||||
|
$.info('开始同步所有远程配置...');
|
||||||
|
try {
|
||||||
|
await syncArtifacts();
|
||||||
success(res);
|
success(res);
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
|
$.error(`同步配置失败,原因:${e.message ?? e}`);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
`FAILED_TO_SYNC_ARTIFACTS`,
|
`FAILED_TO_SYNC_ARTIFACTS`,
|
||||||
`Failed to sync all artifacts`,
|
`Failed to sync all artifacts`,
|
||||||
`Reason: ${err}`,
|
`Reason: ${e.message ?? e}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
$.info(`同步订阅失败,原因:${err}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncArtifact(req, res) {
|
async function syncArtifact(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
$.info(`开始同步远程配置 ${name}...`);
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const artifact = findByName(allArtifacts, name);
|
const artifact = findByName(allArtifacts, name);
|
||||||
|
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
|
$.error(`找不到远程配置 ${name}`);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new ResourceNotFoundError(
|
new ResourceNotFoundError(
|
||||||
'RESOURCE_NOT_FOUND',
|
'RESOURCE_NOT_FOUND',
|
||||||
`Artifact ${name} does not exist!`,
|
`找不到远程配置 ${name}`,
|
||||||
|
),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artifact.source) {
|
||||||
|
$.error(`远程配置 ${name} 未设置来源`);
|
||||||
|
failed(
|
||||||
|
res,
|
||||||
|
new ResourceNotFoundError(
|
||||||
|
'RESOURCE_HAS_NO_SOURCE',
|
||||||
|
`远程配置 ${name} 未设置来源`,
|
||||||
),
|
),
|
||||||
404,
|
404,
|
||||||
);
|
);
|
||||||
@@ -330,10 +761,19 @@ async function syncArtifact(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const useMihomoExternal = artifact.platform === 'SurgeMac';
|
||||||
|
|
||||||
|
if (useMihomoExternal) {
|
||||||
|
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
|
||||||
|
}
|
||||||
const output = await produceArtifact({
|
const output = await produceArtifact({
|
||||||
type: artifact.type,
|
type: artifact.type,
|
||||||
name: artifact.source,
|
name: artifact.source,
|
||||||
platform: artifact.platform,
|
platform: artifact.platform,
|
||||||
|
produceOpts: {
|
||||||
|
'include-unsupported-proxy': artifact.includeUnsupportedProxy,
|
||||||
|
useMihomoExternal,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
$.info(
|
$.info(
|
||||||
@@ -343,6 +783,8 @@ async function syncArtifact(req, res) {
|
|||||||
2,
|
2,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
// if (!output || output.length === 0)
|
||||||
|
// throw new Error('该配置的结果为空 不进行上传');
|
||||||
const resp = await syncToGist({
|
const resp = await syncToGist({
|
||||||
[encodeURIComponent(artifact.name)]: {
|
[encodeURIComponent(artifact.name)]: {
|
||||||
content: output,
|
content: output,
|
||||||
@@ -350,12 +792,38 @@ async function syncArtifact(req, res) {
|
|||||||
});
|
});
|
||||||
artifact.updated = new Date().getTime();
|
artifact.updated = new Date().getTime();
|
||||||
const body = JSON.parse(resp.body);
|
const body = JSON.parse(resp.body);
|
||||||
artifact.url = body.files[
|
|
||||||
encodeURIComponent(artifact.name)
|
delete body.history;
|
||||||
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
delete body.forks;
|
||||||
|
delete body.owner;
|
||||||
|
Object.values(body.files).forEach((file) => {
|
||||||
|
delete file.content;
|
||||||
|
});
|
||||||
|
$.info('上传配置响应:');
|
||||||
|
$.info(JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
|
let files = body.files;
|
||||||
|
let isGitLab;
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
isGitLab = true;
|
||||||
|
files = Object.fromEntries(files.map((item) => [item.path, item]));
|
||||||
|
}
|
||||||
|
const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;
|
||||||
|
const new_url = isGitLab
|
||||||
|
? raw_url
|
||||||
|
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
|
||||||
|
$.info(
|
||||||
|
`上传配置完成\n文件列表: ${Object.keys(files).join(
|
||||||
|
', ',
|
||||||
|
)}\n当前文件: ${encodeURIComponent(
|
||||||
|
artifact.name,
|
||||||
|
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
|
||||||
|
);
|
||||||
|
artifact.url = new_url;
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
success(res, artifact);
|
success(res, artifact);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
$.error(`远程配置 ${artifact.name} 发生错误: ${err.message ?? err}`);
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new InternalServerError(
|
new InternalServerError(
|
||||||
@@ -367,4 +835,4 @@ async function syncArtifact(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { produceArtifact };
|
export { produceArtifact, syncArtifacts };
|
||||||
|
|||||||
181
backend/src/restful/token.js
Normal file
181
backend/src/restful/token.js
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { deleteByName } from '@/utils/database';
|
||||||
|
import { ENV } from '@/vendor/open-api';
|
||||||
|
import { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants';
|
||||||
|
import { failed, success } from '@/restful/response';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import { RequestInvalidError, InternalServerError } from '@/restful/errors';
|
||||||
|
|
||||||
|
export default function register($app) {
|
||||||
|
if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY);
|
||||||
|
|
||||||
|
$app.post('/api/token', signToken);
|
||||||
|
|
||||||
|
$app.route('/api/token/:token').delete(deleteToken);
|
||||||
|
|
||||||
|
$app.route('/api/tokens').get(getAllTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteToken(req, res) {
|
||||||
|
let { token } = req.params;
|
||||||
|
token = decodeURIComponent(token);
|
||||||
|
$.info(`正在删除:${token}`);
|
||||||
|
let allTokens = $.read(TOKENS_KEY);
|
||||||
|
deleteByName(allTokens, token, 'token');
|
||||||
|
$.write(allTokens, TOKENS_KEY);
|
||||||
|
success(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllTokens(req, res) {
|
||||||
|
const { type, name } = req.query;
|
||||||
|
const allTokens = $.read(TOKENS_KEY) || [];
|
||||||
|
success(
|
||||||
|
res,
|
||||||
|
type || name
|
||||||
|
? allTokens.filter(
|
||||||
|
(item) =>
|
||||||
|
(type ? item.type === type : true) &&
|
||||||
|
(name ? item.name === name : true),
|
||||||
|
)
|
||||||
|
: allTokens,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signToken(req, res) {
|
||||||
|
if (!ENV().isNode) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_ENV',
|
||||||
|
`This endpoint is only available in Node.js environment`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { payload, options } = req.body;
|
||||||
|
const ms = eval(`require("ms")`);
|
||||||
|
let token = payload?.token;
|
||||||
|
if (token != null) {
|
||||||
|
if (typeof token !== 'string' || token.length < 1) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_CUSTOM_TOKEN',
|
||||||
|
`Invalid custom token: ${token}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const tokens = $.read(TOKENS_KEY) || [];
|
||||||
|
if (tokens.find((t) => t.token === token)) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'DUPLICATE_TOKEN',
|
||||||
|
`Token ${token} already exists`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const type = payload?.type;
|
||||||
|
const name = payload?.name;
|
||||||
|
if (!type || !name)
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_PAYLOAD',
|
||||||
|
`payload type and name are required`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (type === 'col') {
|
||||||
|
const collections = $.read(COLLECTIONS_KEY) || [];
|
||||||
|
const collection = collections.find((c) => c.name === name);
|
||||||
|
if (!collection)
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_COLLECTION',
|
||||||
|
`collection ${name} not found`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (type === 'file') {
|
||||||
|
const files = $.read(FILES_KEY) || [];
|
||||||
|
const file = files.find((f) => f.name === name);
|
||||||
|
if (!file)
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_FILE',
|
||||||
|
`file ${name} not found`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (type === 'sub') {
|
||||||
|
const subs = $.read(SUBS_KEY) || [];
|
||||||
|
const sub = subs.find((s) => s.name === name);
|
||||||
|
if (!sub)
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_SUB',
|
||||||
|
`sub ${name} not found`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_TYPE',
|
||||||
|
`type ${name} not supported`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let expiresIn = options?.expiresIn;
|
||||||
|
if (options?.expiresIn != null) {
|
||||||
|
expiresIn = ms(options.expiresIn);
|
||||||
|
if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new RequestInvalidError(
|
||||||
|
'INVALID_EXPIRES_IN',
|
||||||
|
`Invalid expiresIn option: ${options.expiresIn}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
|
||||||
|
const nanoid = eval(`require("nanoid")`);
|
||||||
|
const tokens = $.read(TOKENS_KEY) || [];
|
||||||
|
// const now = Date.now();
|
||||||
|
// for (const key in tokens) {
|
||||||
|
// const token = tokens[key];
|
||||||
|
// if (token.exp != null || token.exp < now) {
|
||||||
|
// delete tokens[key];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
if (!token) {
|
||||||
|
do {
|
||||||
|
token = nanoid.customAlphabet(nanoid.urlAlphabet)();
|
||||||
|
} while (tokens.find((t) => t.token === token));
|
||||||
|
}
|
||||||
|
tokens.push({
|
||||||
|
...payload,
|
||||||
|
token,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
expiresIn: expiresIn > 0 ? options?.expiresIn : undefined,
|
||||||
|
exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
$.write(tokens, TOKENS_KEY);
|
||||||
|
return success(res, {
|
||||||
|
token,
|
||||||
|
// secret,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return failed(
|
||||||
|
res,
|
||||||
|
new InternalServerError(
|
||||||
|
'TOKEN_SIGN_FAILED',
|
||||||
|
`Failed to sign token`,
|
||||||
|
`Reason: ${e.message ?? e}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
export function findByName(list, name) {
|
export function findByName(list, name, field = 'name') {
|
||||||
return list.find((item) => item.name === name);
|
return list.find((item) => item[field] === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findIndexByName(list, name) {
|
export function findIndexByName(list, name, field = 'name') {
|
||||||
return list.findIndex((item) => item.name === name);
|
return list.findIndex((item) => item[field] === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteByName(list, name) {
|
export function deleteByName(list, name, field = 'name') {
|
||||||
const idx = findIndexByName(list, name);
|
const idx = findIndexByName(list, name, field);
|
||||||
list.splice(idx, 1);
|
list.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateByName(list, name, newItem) {
|
export function updateByName(list, name, newItem, field = 'name') {
|
||||||
const idx = findIndexByName(list, name);
|
const idx = findIndexByName(list, name, field);
|
||||||
list[idx] = newItem;
|
list[idx] = newItem;
|
||||||
}
|
}
|
||||||
|
|||||||
50
backend/src/utils/dns.js
Normal file
50
backend/src/utils/dns.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import $ from '@/core/app';
|
||||||
|
import dnsPacket from 'dns-packet';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import { isIPv4 } from '@/utils';
|
||||||
|
|
||||||
|
export async function doh({ url, domain, type = 'A', timeout, edns }) {
|
||||||
|
const buf = dnsPacket.encode({
|
||||||
|
type: 'query',
|
||||||
|
id: 0,
|
||||||
|
flags: dnsPacket.RECURSION_DESIRED,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
name: domain,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
additionals: [
|
||||||
|
{
|
||||||
|
type: 'OPT',
|
||||||
|
name: '.',
|
||||||
|
udpPayloadSize: 4096,
|
||||||
|
flags: 0,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
code: 'CLIENT_SUBNET',
|
||||||
|
ip: edns,
|
||||||
|
sourcePrefixLength: isIPv4(edns) ? 24 : 56,
|
||||||
|
scopePrefixLength: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const res = await $.http.get({
|
||||||
|
url: `${url}?dns=${buf
|
||||||
|
.toString('base64')
|
||||||
|
.toString('utf-8')
|
||||||
|
.replace(/=/g, '')}`,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/dns-message',
|
||||||
|
// 'Content-Type': 'application/dns-message',
|
||||||
|
},
|
||||||
|
// body: buf,
|
||||||
|
'binary-mode': true,
|
||||||
|
encoding: null, // 使用 null 编码以确保响应是原始二进制数据
|
||||||
|
timeout,
|
||||||
|
});
|
||||||
|
|
||||||
|
return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body));
|
||||||
|
}
|
||||||
@@ -1,15 +1,37 @@
|
|||||||
import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants';
|
import { SETTINGS_KEY } from '@/constants';
|
||||||
import { findByName } from '@/utils/database';
|
|
||||||
import { HTTP, ENV } from '@/vendor/open-api';
|
import { HTTP, ENV } from '@/vendor/open-api';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
|
import { getPolicyDescriptor } from '@/utils';
|
||||||
import resourceCache from '@/utils/resource-cache';
|
import resourceCache from '@/utils/resource-cache';
|
||||||
|
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||||
|
import {
|
||||||
|
getFlowField,
|
||||||
|
getFlowHeaders,
|
||||||
|
parseFlowHeaders,
|
||||||
|
validCheck,
|
||||||
|
} from '@/utils/flow';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
|
||||||
|
const clashPreprocessor = PROXY_PREPROCESSORS.find(
|
||||||
|
(processor) => processor.name === 'Clash Pre-processor',
|
||||||
|
);
|
||||||
|
|
||||||
const tasks = new Map();
|
const tasks = new Map();
|
||||||
|
|
||||||
export default async function download(url, ua) {
|
export default async function download(
|
||||||
|
rawUrl = '',
|
||||||
|
ua,
|
||||||
|
timeout,
|
||||||
|
customProxy,
|
||||||
|
skipCustomCache,
|
||||||
|
awaitCustomCache,
|
||||||
|
noCache,
|
||||||
|
preprocess,
|
||||||
|
) {
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
|
let url = rawUrl.replace(/#noFlow$/, '');
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
|
url = url.split('#')[0];
|
||||||
if (rawArgs.length > 1) {
|
if (rawArgs.length > 1) {
|
||||||
try {
|
try {
|
||||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||||
@@ -26,60 +48,225 @@ export default async function download(url, ua) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||||
|
const {
|
||||||
|
defaultProxy,
|
||||||
|
defaultUserAgent,
|
||||||
|
defaultTimeout,
|
||||||
|
cacheThreshold: defaultCacheThreshold,
|
||||||
|
} = $.read(SETTINGS_KEY);
|
||||||
|
const cacheThreshold = defaultCacheThreshold || 1024;
|
||||||
|
let proxy = customProxy || defaultProxy;
|
||||||
|
if ($.env.isNode) {
|
||||||
|
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
|
||||||
|
}
|
||||||
|
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||||
|
const requestTimeout = timeout || defaultTimeout || 8000;
|
||||||
|
const id = hex_md5(userAgent + url);
|
||||||
|
|
||||||
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
if ($arguments?.cacheKey === true) {
|
||||||
if (downloadUrlMatch) {
|
$.error(`使用自定义缓存时 cacheKey 的值不能为空`);
|
||||||
let type = downloadUrlMatch?.[1];
|
$arguments.cacheKey = undefined;
|
||||||
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 customCacheKey = $arguments?.cacheKey
|
||||||
const { defaultUserAgent } = $.read(SETTINGS_KEY);
|
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
|
||||||
ua = ua || defaultUserAgent || 'clash.meta';
|
: undefined;
|
||||||
const id = hex_md5(ua + url);
|
|
||||||
|
if (customCacheKey && !skipCustomCache) {
|
||||||
|
const customCached = $.read(customCacheKey);
|
||||||
|
const cached = resourceCache.get(id);
|
||||||
|
if (!noCache && !$arguments?.noCache && cached) {
|
||||||
|
$.info(
|
||||||
|
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
|
||||||
|
);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
if (customCached) {
|
||||||
|
if (awaitCustomCache) {
|
||||||
|
$.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
|
||||||
|
try {
|
||||||
|
await download(
|
||||||
|
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||||
|
ua,
|
||||||
|
timeout,
|
||||||
|
proxy,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
preprocess,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`乐观缓存: URL ${url} 更新缓存发生错误 ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
$.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
|
||||||
|
resourceCache.set(id, customCached);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$.info(
|
||||||
|
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
|
||||||
|
);
|
||||||
|
download(
|
||||||
|
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||||
|
ua,
|
||||||
|
timeout,
|
||||||
|
proxy,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
preprocess,
|
||||||
|
).catch((e) => {
|
||||||
|
$.error(
|
||||||
|
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return customCached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
if (!isNode && tasks.has(id)) {
|
if (!isNode && tasks.has(id)) {
|
||||||
return tasks.get(id);
|
return tasks.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const http = HTTP({
|
const http = HTTP({
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': ua,
|
'User-Agent': userAgent,
|
||||||
|
...(isStash && proxy
|
||||||
|
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
|
||||||
|
: {}),
|
||||||
|
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
|
||||||
},
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = new Promise((resolve, reject) => {
|
let result;
|
||||||
// try to find in app cache
|
|
||||||
const cached = resourceCache.get(id);
|
// try to find in app cache
|
||||||
if (!$arguments?.noCache && cached) {
|
const cached = resourceCache.get(id);
|
||||||
resolve(cached);
|
if (!noCache && !$arguments?.noCache && cached) {
|
||||||
} else {
|
$.info(`使用缓存: ${url}, ${userAgent}`);
|
||||||
$.info(`Downloading...\nUser-Agent: ${ua}\nURL: ${url}`);
|
result = cached;
|
||||||
http.get(url)
|
if (customCacheKey) {
|
||||||
.then((resp) => {
|
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
|
||||||
const body = resp.body;
|
$.write(cached, customCacheKey);
|
||||||
if (body.replace(/\s/g, '').length === 0)
|
|
||||||
reject(new Error('远程资源内容为空!'));
|
|
||||||
else {
|
|
||||||
resourceCache.set(id, body);
|
|
||||||
resolve(body);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
reject(new Error(`无法下载 URL:${url}`));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
const insecure = $arguments?.insecure
|
||||||
|
? isNode
|
||||||
|
? { strictSSL: false }
|
||||||
|
: { insecure: true }
|
||||||
|
: undefined;
|
||||||
|
$.info(
|
||||||
|
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
let { body, headers, statusCode } = await http.get({
|
||||||
|
url,
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
...(insecure ? insecure : {}),
|
||||||
|
});
|
||||||
|
$.info(`statusCode: ${statusCode}`);
|
||||||
|
if (statusCode < 200 || statusCode >= 400) {
|
||||||
|
throw new Error(`statusCode: ${statusCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
const flowInfo = getFlowField(headers);
|
||||||
|
if (flowInfo) {
|
||||||
|
headersResourceCache.set(id, flowInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.replace(/\s/g, '').length === 0)
|
||||||
|
throw new Error(new Error('远程资源内容为空'));
|
||||||
|
if (preprocess) {
|
||||||
|
try {
|
||||||
|
if (clashPreprocessor.test(body)) {
|
||||||
|
body = clashPreprocessor.parse(body, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`Clash Pre-processor error: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let shouldCache = true;
|
||||||
|
if (cacheThreshold) {
|
||||||
|
const size = body.length / 1024;
|
||||||
|
if (size > cacheThreshold) {
|
||||||
|
$.info(
|
||||||
|
`资源大小 ${size.toFixed(
|
||||||
|
2,
|
||||||
|
)} KB 超过了 ${cacheThreshold} KB, 不缓存`,
|
||||||
|
);
|
||||||
|
shouldCache = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldCache) {
|
||||||
|
resourceCache.set(id, body);
|
||||||
|
if (customCacheKey) {
|
||||||
|
$.info(
|
||||||
|
`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`,
|
||||||
|
);
|
||||||
|
$.write(body, customCacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = body;
|
||||||
|
} catch (e) {
|
||||||
|
if (customCacheKey) {
|
||||||
|
const cached = $.read(customCacheKey);
|
||||||
|
if (cached) {
|
||||||
|
$.info(
|
||||||
|
`无法下载 URL ${url}: ${
|
||||||
|
e.message ?? e
|
||||||
|
}\n使用自定义缓存 ${$arguments?.cacheKey}`,
|
||||||
|
);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订阅有效性
|
||||||
|
|
||||||
|
if ($arguments?.validCheck) {
|
||||||
|
await validCheck(
|
||||||
|
parseFlowHeaders(
|
||||||
|
await getFlowHeaders(
|
||||||
|
url,
|
||||||
|
$arguments.flowUserAgent,
|
||||||
|
undefined,
|
||||||
|
proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isNode) {
|
if (!isNode) {
|
||||||
tasks.set(id, result);
|
tasks.set(id, result);
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { version as substoreVersion } from '../../package.json';
|
import { version as substoreVersion } from '../../package.json';
|
||||||
import { ENV } from '@/vendor/open-api';
|
import { ENV } from '@/vendor/open-api';
|
||||||
|
|
||||||
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
|
const {
|
||||||
|
isNode,
|
||||||
|
isQX,
|
||||||
|
isLoon,
|
||||||
|
isSurge,
|
||||||
|
isStash,
|
||||||
|
isShadowRocket,
|
||||||
|
isLanceX,
|
||||||
|
isEgern,
|
||||||
|
isGUIforCores,
|
||||||
|
} = ENV();
|
||||||
let backend = 'Node';
|
let backend = 'Node';
|
||||||
if (isNode) backend = 'Node';
|
if (isNode) backend = 'Node';
|
||||||
if (isQX) backend = 'QX';
|
if (isQX) backend = 'QX';
|
||||||
@@ -9,8 +19,51 @@ if (isLoon) backend = 'Loon';
|
|||||||
if (isSurge) backend = 'Surge';
|
if (isSurge) backend = 'Surge';
|
||||||
if (isStash) backend = 'Stash';
|
if (isStash) backend = 'Stash';
|
||||||
if (isShadowRocket) backend = 'ShadowRocket';
|
if (isShadowRocket) backend = 'ShadowRocket';
|
||||||
|
if (isEgern) backend = 'Egern';
|
||||||
|
if (isLanceX) backend = 'LanceX';
|
||||||
|
if (isGUIforCores) backend = 'GUI.for.Cores';
|
||||||
|
|
||||||
|
let meta = {};
|
||||||
|
let feature = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof $environment !== 'undefined') {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
meta.env = $environment;
|
||||||
|
}
|
||||||
|
if (typeof $loon !== 'undefined') {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
meta.loon = $loon;
|
||||||
|
}
|
||||||
|
if (typeof $script !== 'undefined') {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
meta.script = $script;
|
||||||
|
}
|
||||||
|
if (typeof $Plugin !== 'undefined') {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
meta.plugin = $Plugin;
|
||||||
|
}
|
||||||
|
if (isNode) {
|
||||||
|
meta.node = {
|
||||||
|
version: eval('process.version'),
|
||||||
|
argv: eval('process.argv'),
|
||||||
|
filename: eval('__filename'),
|
||||||
|
dirname: eval('__dirname'),
|
||||||
|
env: {},
|
||||||
|
};
|
||||||
|
const env = eval('process.env');
|
||||||
|
for (const key in env) {
|
||||||
|
if (/^SUB_STORE_/.test(key)) {
|
||||||
|
meta.node.env[key] = env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
backend,
|
backend,
|
||||||
version: substoreVersion,
|
version: substoreVersion,
|
||||||
|
feature,
|
||||||
|
meta,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,43 +1,353 @@
|
|||||||
import { HTTP } from '@/vendor/open-api';
|
import { SETTINGS_KEY } from '@/constants';
|
||||||
|
import { HTTP, ENV } from '@/vendor/open-api';
|
||||||
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
|
import { getPolicyDescriptor } from '@/utils';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||||
|
|
||||||
export async function getFlowHeaders(url) {
|
export function getFlowField(headers) {
|
||||||
const http = HTTP();
|
const keys = Object.keys(headers);
|
||||||
const { headers } = await http.get({
|
let sub = '';
|
||||||
url: url
|
let webPage = '';
|
||||||
.split(/[\r\n]+/)
|
for (let k of keys) {
|
||||||
.map((i) => i.trim())
|
const lower = k.toLowerCase();
|
||||||
.filter((i) => i.length)[0],
|
if (lower === 'subscription-userinfo') {
|
||||||
headers: {
|
sub = headers[k];
|
||||||
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
|
} else if (lower === 'profile-web-page-url') {
|
||||||
},
|
webPage = headers[k];
|
||||||
});
|
}
|
||||||
const subkey = Object.keys(headers).filter((k) =>
|
}
|
||||||
/SUBSCRIPTION-USERINFO/i.test(k),
|
|
||||||
)[0];
|
return `${sub || ''}${
|
||||||
return headers[subkey];
|
webPage ? `; app_url=${encodeURIComponent(webPage)}` : ''
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
export async function getFlowHeaders(
|
||||||
|
rawUrl,
|
||||||
|
ua,
|
||||||
|
timeout,
|
||||||
|
customProxy,
|
||||||
|
flowUrl,
|
||||||
|
) {
|
||||||
|
let url = flowUrl || rawUrl || '';
|
||||||
|
let $arguments = {};
|
||||||
|
const rawArgs = url.split('#');
|
||||||
|
url = url.split('#')[0];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($arguments?.noFlow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||||
|
const insecure = $arguments?.insecure
|
||||||
|
? $.env.isNode
|
||||||
|
? { strictSSL: false }
|
||||||
|
: { insecure: true }
|
||||||
|
: undefined;
|
||||||
|
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
|
||||||
|
$.read(SETTINGS_KEY);
|
||||||
|
let proxy = customProxy || defaultProxy;
|
||||||
|
if ($.env.isNode) {
|
||||||
|
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
|
||||||
|
}
|
||||||
|
const userAgent = ua || defaultFlowUserAgent || 'clash';
|
||||||
|
const requestTimeout = timeout || defaultTimeout || 8000;
|
||||||
|
const id = hex_md5(userAgent + url);
|
||||||
|
const cached = headersResourceCache.get(id);
|
||||||
|
let flowInfo;
|
||||||
|
if (!$arguments?.noCache && cached) {
|
||||||
|
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
|
||||||
|
flowInfo = cached;
|
||||||
|
} else {
|
||||||
|
const http = HTTP();
|
||||||
|
if (flowUrl) {
|
||||||
|
$.info(
|
||||||
|
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
|
||||||
|
);
|
||||||
|
const { body } = await http.get({
|
||||||
|
url: flowUrl,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
...(insecure ? insecure : {}),
|
||||||
|
});
|
||||||
|
flowInfo = body;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$.info(
|
||||||
|
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
|
||||||
|
);
|
||||||
|
const { headers } = await http.head({
|
||||||
|
url: url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)[0],
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
...(isStash && proxy
|
||||||
|
? {
|
||||||
|
'X-Stash-Selected-Proxy':
|
||||||
|
encodeURIComponent(proxy),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(isShadowRocket && proxy
|
||||||
|
? { 'X-Surge-Policy': proxy }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
...(insecure ? insecure : {}),
|
||||||
|
});
|
||||||
|
flowInfo = getFlowField(headers);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!flowInfo) {
|
||||||
|
$.info(
|
||||||
|
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
|
||||||
|
);
|
||||||
|
const { headers } = await http.get({
|
||||||
|
url: url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)[0],
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
...(isStash && proxy
|
||||||
|
? {
|
||||||
|
'X-Stash-Selected-Proxy':
|
||||||
|
encodeURIComponent(proxy),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(isShadowRocket && proxy
|
||||||
|
? { 'X-Surge-Policy': proxy }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
...(insecure ? insecure : {}),
|
||||||
|
});
|
||||||
|
flowInfo = getFlowField(headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (flowInfo) {
|
||||||
|
flowInfo = flowInfo.trim();
|
||||||
|
}
|
||||||
|
if (flowInfo) {
|
||||||
|
headersResourceCache.set(id, flowInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flowInfo;
|
||||||
}
|
}
|
||||||
export function parseFlowHeaders(flowHeaders) {
|
export function parseFlowHeaders(flowHeaders) {
|
||||||
if (!flowHeaders) return;
|
if (!flowHeaders) return;
|
||||||
// unit is KB
|
// unit is KB
|
||||||
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
|
const uploadMatch = flowHeaders.match(
|
||||||
|
/upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||||
|
);
|
||||||
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
||||||
|
|
||||||
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
|
const downloadMatch = flowHeaders.match(
|
||||||
|
/download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||||
|
);
|
||||||
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
||||||
|
const totalMatch = flowHeaders.match(
|
||||||
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
|
/total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||||
|
);
|
||||||
|
const total = Number(totalMatch[1] + totalMatch[2]);
|
||||||
|
|
||||||
// optional expire timestamp
|
// optional expire timestamp
|
||||||
const match = flowHeaders.match(/expire=(\d+)/);
|
const expireMatch = flowHeaders.match(
|
||||||
const expires = match ? Number(match[1]) : undefined;
|
/expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||||
|
);
|
||||||
|
const expires = expireMatch
|
||||||
|
? Number(expireMatch[1] + expireMatch[2])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return { expires, total, usage: { upload, download } };
|
const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
|
||||||
|
const remainingDays = remainingDaysMatch
|
||||||
|
? Number(remainingDaysMatch[1])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
|
||||||
|
const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
|
||||||
|
|
||||||
|
const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
|
||||||
|
const planName = planNameMatch
|
||||||
|
? decodeURIComponent(planNameMatch[1])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
expires,
|
||||||
|
total,
|
||||||
|
usage: { upload, download },
|
||||||
|
remainingDays,
|
||||||
|
appUrl,
|
||||||
|
planName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flowTransfer(flow, unit = 'B') {
|
export function flowTransfer(flow, unit = 'B') {
|
||||||
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
|
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
let unitIndex = unitList.indexOf(unit);
|
let unitIndex = unitList.indexOf(unit);
|
||||||
|
|
||||||
return flow < 1024
|
return flow < 1024 || unitIndex === unitList.length - 1
|
||||||
? { value: flow.toFixed(1), unit: unit }
|
? { value: flow.toFixed(1), unit: unit }
|
||||||
: flowTransfer(flow / 1024, unitList[++unitIndex]);
|
: flowTransfer(flow / 1024, unitList[++unitIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validCheck(flow) {
|
||||||
|
if (!flow) {
|
||||||
|
throw new Error('没有流量信息');
|
||||||
|
}
|
||||||
|
if (flow?.expires && flow.expires * 1000 < Date.now()) {
|
||||||
|
const date = new Date(flow.expires * 1000).toLocaleDateString();
|
||||||
|
throw new Error(`订阅已过期: ${date}`);
|
||||||
|
}
|
||||||
|
if (flow?.total) {
|
||||||
|
const upload = flow.usage?.upload || 0;
|
||||||
|
const download = flow.usage?.download || 0;
|
||||||
|
if (flow.total - upload - download < 0) {
|
||||||
|
const current = upload + download;
|
||||||
|
const currT = flowTransfer(Math.abs(current));
|
||||||
|
currT.value = current < 0 ? '-' + currT.value : currT.value;
|
||||||
|
const totalT = flowTransfer(flow.total);
|
||||||
|
throw new Error(
|
||||||
|
`流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRmainingDays(opt = {}) {
|
||||||
|
try {
|
||||||
|
let { resetDay, startDate, cycleDays } = opt;
|
||||||
|
if (['string', 'number'].includes(typeof opt)) {
|
||||||
|
resetDay = opt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate && cycleDays) {
|
||||||
|
cycleDays = parseInt(cycleDays);
|
||||||
|
if (isNaN(cycleDays) || cycleDays <= 0)
|
||||||
|
throw new Error('重置周期应为正整数');
|
||||||
|
if (!startDate || !Date.parse(startDate))
|
||||||
|
throw new Error('开始日期不合法');
|
||||||
|
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const today = new Date();
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
if (start.getTime() > today.getTime())
|
||||||
|
throw new Error('开始日期应早于现在');
|
||||||
|
|
||||||
|
let resetDate = new Date(startDate);
|
||||||
|
resetDate.setDate(resetDate.getDate() + cycleDays);
|
||||||
|
|
||||||
|
while (resetDate < today) {
|
||||||
|
resetDate.setDate(resetDate.getDate() + cycleDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDate.setHours(0, 0, 0, 0);
|
||||||
|
const timeDiff = resetDate.getTime() - today.getTime();
|
||||||
|
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||||
|
|
||||||
|
return daysDiff;
|
||||||
|
} else {
|
||||||
|
if (!resetDay) return;
|
||||||
|
resetDay = parseInt(resetDay);
|
||||||
|
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
|
||||||
|
throw new Error('月重置日应为 1-31 之间的整数');
|
||||||
|
let now = new Date();
|
||||||
|
let today = now.getDate();
|
||||||
|
let month = now.getMonth();
|
||||||
|
let year = now.getFullYear();
|
||||||
|
let daysInMonth;
|
||||||
|
|
||||||
|
if (resetDay > today) {
|
||||||
|
daysInMonth = 0;
|
||||||
|
} else {
|
||||||
|
daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return daysInMonth - today + resetDay;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`getRmainingDays failed: ${e.message ?? e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeFlowHeader(flowHeaders) {
|
||||||
|
try {
|
||||||
|
// 使用 Map 保持顺序并处理重复键
|
||||||
|
const kvMap = new Map();
|
||||||
|
|
||||||
|
flowHeaders
|
||||||
|
.split(';')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach((pair) => {
|
||||||
|
const eqIndex = pair.indexOf('=');
|
||||||
|
if (eqIndex === -1) return;
|
||||||
|
|
||||||
|
const key = pair.slice(0, eqIndex).trim();
|
||||||
|
const encodedValue = pair.slice(eqIndex + 1).trim();
|
||||||
|
|
||||||
|
// 只保留第一个出现的 key
|
||||||
|
if (!kvMap.has(key)) {
|
||||||
|
try {
|
||||||
|
// 解码 URI 组件并保留原始值作为 fallback
|
||||||
|
const decodedValue = decodeURIComponent(encodedValue);
|
||||||
|
kvMap.set(key, decodedValue);
|
||||||
|
} catch (e) {
|
||||||
|
kvMap.set(key, encodedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拼接标准化字符串
|
||||||
|
return Array.from(kvMap.entries())
|
||||||
|
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
|
||||||
|
.join('; ');
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
|
||||||
|
return flowHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,116 @@
|
|||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
|
const ISOFlags = {
|
||||||
|
'🏳️🌈': ['EXP', 'BAND'],
|
||||||
|
'🇸🇱': ['TEST', 'SOS'],
|
||||||
|
'🇦🇩': ['AD', 'AND'],
|
||||||
|
'🇦🇪': ['AE', 'ARE'],
|
||||||
|
'🇦🇫': ['AF', 'AFG'],
|
||||||
|
'🇦🇱': ['AL', 'ALB'],
|
||||||
|
'🇦🇲': ['AM', 'ARM'],
|
||||||
|
'🇦🇷': ['AR', 'ARG'],
|
||||||
|
'🇦🇹': ['AT', 'AUT'],
|
||||||
|
'🇦🇺': ['AU', 'AUS'],
|
||||||
|
'🇦🇿': ['AZ', 'AZE'],
|
||||||
|
'🇧🇦': ['BA', 'BIH'],
|
||||||
|
'🇧🇩': ['BD', 'BGD'],
|
||||||
|
'🇧🇪': ['BE', 'BEL'],
|
||||||
|
'🇧🇬': ['BG', 'BGR'],
|
||||||
|
'🇧🇭': ['BH', 'BHR'],
|
||||||
|
'🇧🇴': ['BO', 'BOL'],
|
||||||
|
'🇧🇳': ['BN', 'BRN'],
|
||||||
|
'🇧🇷': ['BR', 'BRA'],
|
||||||
|
'🇧🇹': ['BT', 'BTN'],
|
||||||
|
'🇧🇾': ['BY', 'BLR'],
|
||||||
|
'🇨🇦': ['CA', 'CAN'],
|
||||||
|
'🇨🇭': ['CH', 'CHE'],
|
||||||
|
'🇨🇱': ['CL', 'CHL'],
|
||||||
|
'🇨🇴': ['CO', 'COL'],
|
||||||
|
'🇨🇷': ['CR', 'CRI'],
|
||||||
|
'🇨🇾': ['CY', 'CYP'],
|
||||||
|
'🇨🇿': ['CZ', 'CZE'],
|
||||||
|
'🇩🇪': ['DE', 'DEU'],
|
||||||
|
'🇩🇰': ['DK', 'DNK'],
|
||||||
|
'🇪🇨': ['EC', 'ECU'],
|
||||||
|
'🇪🇪': ['EE', 'EST'],
|
||||||
|
'🇪🇬': ['EG', 'EGY'],
|
||||||
|
'🇪🇸': ['ES', 'ESP'],
|
||||||
|
'🇪🇺': ['EU'],
|
||||||
|
'🇫🇮': ['FI', 'FIN'],
|
||||||
|
'🇫🇷': ['FR', 'FRA'],
|
||||||
|
'🇬🇧': ['GB', 'GBR', 'UK'],
|
||||||
|
'🇬🇪': ['GE', 'GEO'],
|
||||||
|
'🇬🇷': ['GR', 'GRC'],
|
||||||
|
'🇬🇹': ['GT', 'GTM'],
|
||||||
|
'🇬🇺': ['GU', 'GUM'],
|
||||||
|
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
|
||||||
|
'🇭🇷': ['HR', 'HRV'],
|
||||||
|
'🇭🇺': ['HU', 'HUN'],
|
||||||
|
'🇯🇴': ['JO', 'JOR'],
|
||||||
|
'🇯🇵': ['JP', 'JPN', 'TYO'],
|
||||||
|
'🇰🇪': ['KE', 'KEN'],
|
||||||
|
'🇰🇬': ['KG', 'KGZ'],
|
||||||
|
'🇰🇭': ['KH', 'KGZ'],
|
||||||
|
'🇰🇵': ['KP', 'PRK'],
|
||||||
|
'🇰🇷': ['KR', 'KOR', 'SEL'],
|
||||||
|
'🇰🇿': ['KZ', 'KAZ'],
|
||||||
|
'🇮🇩': ['ID', 'IDN'],
|
||||||
|
'🇮🇪': ['IE', 'IRL'],
|
||||||
|
'🇮🇱': ['IL', 'ISR'],
|
||||||
|
'🇮🇲': ['IM', 'IMN'],
|
||||||
|
'🇮🇳': ['IN', 'IND'],
|
||||||
|
'🇮🇷': ['IR', 'IRN'],
|
||||||
|
'🇮🇸': ['IS', 'ISL'],
|
||||||
|
'🇮🇹': ['IT', 'ITA'],
|
||||||
|
'🇱🇦': ['LA', 'LAO'],
|
||||||
|
'🇱🇰': ['LK', 'LKA'],
|
||||||
|
'🇱🇹': ['LT', 'LTU'],
|
||||||
|
'🇱🇺': ['LU', 'LUX'],
|
||||||
|
'🇱🇻': ['LV', 'LVA'],
|
||||||
|
'🇲🇦': ['MA', 'MAR'],
|
||||||
|
'🇲🇩': ['MD', 'MDA'],
|
||||||
|
'🇳🇬': ['NG', 'NGA'],
|
||||||
|
'🇲🇲': ['MM', 'MMR'],
|
||||||
|
'🇲🇰': ['MK', 'MKD'],
|
||||||
|
'🇲🇳': ['MN', 'MNG'],
|
||||||
|
'🇲🇴': ['MO', 'MAC', 'CTM'],
|
||||||
|
'🇲🇹': ['MT', 'MLT'],
|
||||||
|
'🇲🇽': ['MX', 'MEX'],
|
||||||
|
'🇲🇾': ['MY', 'MYS'],
|
||||||
|
'🇳🇱': ['NL', 'NLD', 'AMS'],
|
||||||
|
'🇳🇴': ['NO', 'NOR'],
|
||||||
|
'🇳🇵': ['NP', 'NPL'],
|
||||||
|
'🇳🇿': ['NZ', 'NZL'],
|
||||||
|
'🇵🇦': ['PA', 'PAN'],
|
||||||
|
'🇵🇪': ['PE', 'PER'],
|
||||||
|
'🇵🇭': ['PH', 'PHL'],
|
||||||
|
'🇵🇰': ['PK', 'PAK'],
|
||||||
|
'🇵🇱': ['PL', 'POL'],
|
||||||
|
'🇵🇷': ['PR', 'PRI'],
|
||||||
|
'🇵🇹': ['PT', 'PRT'],
|
||||||
|
'🇵🇾': ['PY', 'PRY'],
|
||||||
|
'🇵🇬': ['PG', 'PNG'],
|
||||||
|
'🇷🇴': ['RO', 'ROU'],
|
||||||
|
'🇷🇸': ['RS', 'SRB'],
|
||||||
|
'🇷🇪': ['RE', 'REU'],
|
||||||
|
'🇷🇺': ['RU', 'RUS'],
|
||||||
|
'🇸🇦': ['SA', 'SAU'],
|
||||||
|
'🇸🇪': ['SE', 'SWE'],
|
||||||
|
'🇸🇬': ['SG', 'SGP'],
|
||||||
|
'🇸🇮': ['SI', 'SVN'],
|
||||||
|
'🇸🇰': ['SK', 'SVK'],
|
||||||
|
'🇹🇭': ['TH', 'THA'],
|
||||||
|
'🇹🇳': ['TN', 'TUN'],
|
||||||
|
'🇹🇷': ['TR', 'TUR'],
|
||||||
|
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
|
||||||
|
'🇺🇦': ['UA', 'UKR'],
|
||||||
|
'🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'],
|
||||||
|
'🇺🇾': ['UY', 'URY'],
|
||||||
|
'🇻🇪': ['VE', 'VEN'],
|
||||||
|
'🇻🇳': ['VN', 'VNM'],
|
||||||
|
'🇿🇦': ['ZA', 'ZAF', 'JNB'],
|
||||||
|
'🇨🇳': ['CN', 'CHN', 'BACK'],
|
||||||
|
};
|
||||||
// get proxy flag according to its name
|
// get proxy flag according to its name
|
||||||
export function getFlag(name) {
|
export function getFlag(name) {
|
||||||
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
|
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
|
||||||
@@ -36,7 +149,10 @@ export function getFlag(name) {
|
|||||||
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
|
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
|
||||||
'🇧🇭': ['Bahrain', '巴林'],
|
'🇧🇭': ['Bahrain', '巴林'],
|
||||||
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
|
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
|
||||||
|
'🇧🇳': ['Brunei', '文莱', '汶萊'],
|
||||||
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
|
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
|
||||||
|
'🇧🇴': ['Bolivia', '玻利维亚'],
|
||||||
|
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
|
||||||
'🇨🇦': [
|
'🇨🇦': [
|
||||||
'Canada',
|
'Canada',
|
||||||
'加拿大',
|
'加拿大',
|
||||||
@@ -47,6 +163,7 @@ export function getFlag(name) {
|
|||||||
'滑铁卢',
|
'滑铁卢',
|
||||||
'多伦多',
|
'多伦多',
|
||||||
'Waterloo',
|
'Waterloo',
|
||||||
|
'Toronto',
|
||||||
],
|
],
|
||||||
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
|
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
|
||||||
'🇨🇱': ['Chile', '智利'],
|
'🇨🇱': ['Chile', '智利'],
|
||||||
@@ -65,6 +182,7 @@ export function getFlag(name) {
|
|||||||
'广德',
|
'广德',
|
||||||
'法兰克福',
|
'法兰克福',
|
||||||
'Frankfurt',
|
'Frankfurt',
|
||||||
|
'德意志',
|
||||||
],
|
],
|
||||||
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
|
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
|
||||||
'🇪🇨': ['Ecuador', '厄瓜多尔'],
|
'🇪🇨': ['Ecuador', '厄瓜多尔'],
|
||||||
@@ -85,6 +203,8 @@ export function getFlag(name) {
|
|||||||
],
|
],
|
||||||
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
|
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
|
||||||
'🇬🇷': ['Greece', '希腊', '希臘'],
|
'🇬🇷': ['Greece', '希腊', '希臘'],
|
||||||
|
'🇬🇺': ['Guam', '关岛', '關島'],
|
||||||
|
'🇬🇹': ['Guatemala', '危地马拉'],
|
||||||
'🇭🇰': [
|
'🇭🇰': [
|
||||||
'Hongkong',
|
'Hongkong',
|
||||||
'香港',
|
'香港',
|
||||||
@@ -140,15 +260,18 @@ export function getFlag(name) {
|
|||||||
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
|
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
|
||||||
'🇮🇱': ['Israel', '以色列'],
|
'🇮🇱': ['Israel', '以色列'],
|
||||||
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
|
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
|
||||||
'🇮🇳': ['India', '印度', '孟买', 'MFumbai'],
|
'🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],
|
||||||
'🇮🇷': ['Iran', '伊朗'],
|
'🇮🇷': ['Iran', '伊朗'],
|
||||||
'🇮🇸': ['Iceland', '冰岛', '冰島'],
|
'🇮🇸': ['Iceland', '冰岛', '冰島'],
|
||||||
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
|
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
|
||||||
|
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
|
||||||
|
'🇱🇦': ['Laos', '老挝', '老撾'],
|
||||||
'🇱🇹': ['Lithuania', '立陶宛'],
|
'🇱🇹': ['Lithuania', '立陶宛'],
|
||||||
'🇱🇺': ['Luxembourg', '卢森堡'],
|
'🇱🇺': ['Luxembourg', '卢森堡'],
|
||||||
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
|
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
|
||||||
'🇲🇦': ['Morocco', '摩洛哥'],
|
'🇲🇦': ['Morocco', '摩洛哥'],
|
||||||
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
|
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
|
||||||
|
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
|
||||||
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
|
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
|
||||||
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
|
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
|
||||||
'🇲🇳': ['Mongolia', '蒙古'],
|
'🇲🇳': ['Mongolia', '蒙古'],
|
||||||
@@ -156,7 +279,14 @@ export function getFlag(name) {
|
|||||||
'🇲🇹': ['Malta', '马耳他'],
|
'🇲🇹': ['Malta', '马耳他'],
|
||||||
'🇲🇽': ['Mexico', '墨西哥'],
|
'🇲🇽': ['Mexico', '墨西哥'],
|
||||||
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
|
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
|
||||||
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
|
'🇳🇱': [
|
||||||
|
'Netherlands',
|
||||||
|
'荷兰',
|
||||||
|
'荷蘭',
|
||||||
|
'尼德蘭',
|
||||||
|
'阿姆斯特丹',
|
||||||
|
'Amsterdam',
|
||||||
|
],
|
||||||
'🇳🇴': ['Norway', '挪威'],
|
'🇳🇴': ['Norway', '挪威'],
|
||||||
'🇳🇵': ['Nepal', '尼泊尔'],
|
'🇳🇵': ['Nepal', '尼泊尔'],
|
||||||
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
|
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
|
||||||
@@ -164,9 +294,10 @@ export function getFlag(name) {
|
|||||||
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
|
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
|
||||||
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
|
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
|
||||||
'🇵🇰': ['Pakistan', '巴基斯坦'],
|
'🇵🇰': ['Pakistan', '巴基斯坦'],
|
||||||
'🇵🇱': ['Poland', '波兰', '波蘭'],
|
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
|
||||||
'🇵🇷': ['Puerto Rico', '波多黎各'],
|
'🇵🇷': ['Puerto Rico', '波多黎各'],
|
||||||
'🇵🇹': ['Portugal', '葡萄牙'],
|
'🇵🇹': ['Portugal', '葡萄牙'],
|
||||||
|
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
|
||||||
'🇵🇾': ['Paraguay', '巴拉圭'],
|
'🇵🇾': ['Paraguay', '巴拉圭'],
|
||||||
'🇷🇴': ['Romania', '罗马尼亚'],
|
'🇷🇴': ['Romania', '罗马尼亚'],
|
||||||
'🇷🇸': ['Serbia', '塞尔维亚'],
|
'🇷🇸': ['Serbia', '塞尔维亚'],
|
||||||
@@ -188,8 +319,8 @@ export function getFlag(name) {
|
|||||||
'沪俄',
|
'沪俄',
|
||||||
'Moscow',
|
'Moscow',
|
||||||
],
|
],
|
||||||
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'],
|
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
|
||||||
'🇸🇪': ['Sweden', '瑞典'],
|
'🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],
|
||||||
'🇸🇬': [
|
'🇸🇬': [
|
||||||
'Singapore',
|
'Singapore',
|
||||||
'新加坡',
|
'新加坡',
|
||||||
@@ -209,16 +340,22 @@ export function getFlag(name) {
|
|||||||
'🇸🇰': ['Slovakia', '斯洛伐克'],
|
'🇸🇰': ['Slovakia', '斯洛伐克'],
|
||||||
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
|
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
|
||||||
'🇹🇳': ['Tunisia', '突尼斯'],
|
'🇹🇳': ['Tunisia', '突尼斯'],
|
||||||
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔'],
|
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],
|
||||||
'🇹🇼': [
|
'🇹🇼': [
|
||||||
'Taiwan',
|
'Taiwan',
|
||||||
'台湾',
|
'台湾',
|
||||||
|
'臺灣',
|
||||||
|
'台灣',
|
||||||
|
'中華民國',
|
||||||
|
'中华民国',
|
||||||
'台北',
|
'台北',
|
||||||
'台中',
|
'台中',
|
||||||
'新北',
|
'新北',
|
||||||
'彰化',
|
'彰化',
|
||||||
'台',
|
'台',
|
||||||
|
'臺',
|
||||||
'Taipei',
|
'Taipei',
|
||||||
|
'Tai Wan',
|
||||||
],
|
],
|
||||||
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
|
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
|
||||||
'🇺🇸': [
|
'🇺🇸': [
|
||||||
@@ -230,6 +367,7 @@ export function getFlag(name) {
|
|||||||
'波特兰',
|
'波特兰',
|
||||||
'达拉斯',
|
'达拉斯',
|
||||||
'俄勒冈',
|
'俄勒冈',
|
||||||
|
'Oregon',
|
||||||
'凤凰城',
|
'凤凰城',
|
||||||
'费利蒙',
|
'费利蒙',
|
||||||
'硅谷',
|
'硅谷',
|
||||||
@@ -243,10 +381,17 @@ export function getFlag(name) {
|
|||||||
'沪美',
|
'沪美',
|
||||||
'哥伦布',
|
'哥伦布',
|
||||||
'纽约',
|
'纽约',
|
||||||
|
'New York',
|
||||||
'Los Angeles',
|
'Los Angeles',
|
||||||
'San Jose',
|
'San Jose',
|
||||||
'Sillicon Valley',
|
'Sillicon Valley',
|
||||||
'Michigan',
|
'Michigan',
|
||||||
|
'俄亥俄',
|
||||||
|
'Ohio',
|
||||||
|
'马纳萨斯',
|
||||||
|
'Manassas',
|
||||||
|
'弗吉尼亚',
|
||||||
|
'Virginia',
|
||||||
],
|
],
|
||||||
'🇺🇾': ['Uruguay', '乌拉圭'],
|
'🇺🇾': ['Uruguay', '乌拉圭'],
|
||||||
'🇻🇪': ['Venezuela', '委内瑞拉'],
|
'🇻🇪': ['Venezuela', '委内瑞拉'],
|
||||||
@@ -278,108 +423,6 @@ export function getFlag(name) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const ISOFlags = {
|
|
||||||
'🏳️🌈': ['EXP', 'BAND'],
|
|
||||||
'🇸🇱': ['TEST', 'SOS'],
|
|
||||||
'🇦🇩': ['AD', 'AND'],
|
|
||||||
'🇦🇪': ['AE', 'ARE'],
|
|
||||||
'🇦🇫': ['AF', 'AFG'],
|
|
||||||
'🇦🇱': ['AL', 'ALB'],
|
|
||||||
'🇦🇲': ['AM', 'ARM'],
|
|
||||||
'🇦🇷': ['AR', 'ARG'],
|
|
||||||
'🇦🇹': ['AT', 'AUT'],
|
|
||||||
'🇦🇺': ['AU', 'AUS'],
|
|
||||||
'🇦🇿': ['AZ', 'AZE'],
|
|
||||||
'🇧🇦': ['BA', 'BIH'],
|
|
||||||
'🇧🇩': ['BD', 'BGD'],
|
|
||||||
'🇧🇪': ['BE', 'BEL'],
|
|
||||||
'🇧🇬': ['BG', 'BGR'],
|
|
||||||
'🇧🇭': ['BH', 'BHR'],
|
|
||||||
'🇧🇷': ['BR', 'BRA'],
|
|
||||||
'🇧🇾': ['BY', 'BLR'],
|
|
||||||
'🇨🇦': ['CA', 'CAN'],
|
|
||||||
'🇨🇭': ['CH', 'CHE'],
|
|
||||||
'🇨🇱': ['CL', 'CHL'],
|
|
||||||
'🇨🇴': ['CO', 'COL'],
|
|
||||||
'🇨🇷': ['CR', 'CRI'],
|
|
||||||
'🇨🇾': ['CY', 'CYP'],
|
|
||||||
'🇨🇿': ['CZ', 'CZE'],
|
|
||||||
'🇩🇪': ['DE', 'DEU'],
|
|
||||||
'🇩🇰': ['DK', 'DNK'],
|
|
||||||
'🇪🇨': ['EC', 'ECU'],
|
|
||||||
'🇪🇪': ['EE', 'EST'],
|
|
||||||
'🇪🇬': ['EG', 'EGY'],
|
|
||||||
'🇪🇸': ['ES', 'ESP'],
|
|
||||||
'🇪🇺': ['EU'],
|
|
||||||
'🇫🇮': ['FI', 'FIN'],
|
|
||||||
'🇫🇷': ['FR', 'FRA'],
|
|
||||||
'🇬🇧': ['GB', 'GBR', 'UK'],
|
|
||||||
'🇬🇪': ['GE', 'GEO'],
|
|
||||||
'🇬🇷': ['GR', 'GRC'],
|
|
||||||
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
|
|
||||||
'🇭🇷': ['HR', 'HRV'],
|
|
||||||
'🇭🇺': ['HU', 'HUN'],
|
|
||||||
'🇯🇴': ['JO', 'JOR'],
|
|
||||||
'🇯🇵': ['JP', 'JPN'],
|
|
||||||
'🇰🇪': ['KE', 'KEN'],
|
|
||||||
'🇰🇬': ['KG', 'KGZ'],
|
|
||||||
'🇰🇭': ['KH', 'KGZ'],
|
|
||||||
'🇰🇵': ['KP', 'PRK'],
|
|
||||||
'🇰🇷': ['KR', 'KOR'],
|
|
||||||
'🇰🇿': ['KZ', 'KAZ'],
|
|
||||||
'🇮🇩': ['ID', 'IDN'],
|
|
||||||
'🇮🇪': ['IE', 'IRL'],
|
|
||||||
'🇮🇱': ['IL', 'ISR'],
|
|
||||||
'🇮🇲': ['IM', 'IMN'],
|
|
||||||
'🇮🇳': ['IN', 'IND'],
|
|
||||||
'🇮🇷': ['IR', 'IRN'],
|
|
||||||
'🇮🇸': ['IS', 'ISL'],
|
|
||||||
'🇮🇹': ['IT', 'ITA'],
|
|
||||||
'🇱🇹': ['LT', 'LTU'],
|
|
||||||
'🇱🇺': ['LU', 'LUX'],
|
|
||||||
'🇱🇻': ['LV', 'LVA'],
|
|
||||||
'🇲🇦': ['MA', 'MAR'],
|
|
||||||
'🇲🇩': ['MD', 'MDA'],
|
|
||||||
'🇳🇬': ['NG', 'NGA'],
|
|
||||||
'🇲🇰': ['MK', 'MKD'],
|
|
||||||
'🇲🇳': ['MN', 'MNG'],
|
|
||||||
'🇲🇴': ['MO', 'MAC', 'CTM'],
|
|
||||||
'🇲🇹': ['MT', 'MLT'],
|
|
||||||
'🇲🇽': ['MX', 'MEX'],
|
|
||||||
'🇲🇾': ['MY', 'MYS'],
|
|
||||||
'🇳🇱': ['NL', 'NLD'],
|
|
||||||
'🇳🇴': ['NO', 'NOR'],
|
|
||||||
'🇳🇵': ['NP', 'NPL'],
|
|
||||||
'🇳🇿': ['NZ', 'NZL'],
|
|
||||||
'🇵🇦': ['PA', 'PAN'],
|
|
||||||
'🇵🇪': ['PE', 'PER'],
|
|
||||||
'🇵🇭': ['PH', 'PHL'],
|
|
||||||
'🇵🇰': ['PK', 'PAK'],
|
|
||||||
'🇵🇱': ['PL', 'POL'],
|
|
||||||
'🇵🇷': ['PR', 'PRI'],
|
|
||||||
'🇵🇹': ['PT', 'PRT'],
|
|
||||||
'🇵🇾': ['PY', 'PRY'],
|
|
||||||
'🇷🇴': ['RO', 'ROU'],
|
|
||||||
'🇷🇸': ['RS', 'SRB'],
|
|
||||||
'🇷🇪': ['RE', 'REU'],
|
|
||||||
'🇷🇺': ['RU', 'RUS'],
|
|
||||||
'🇸🇦': ['SA', 'SAU'],
|
|
||||||
'🇸🇪': ['SE', 'SWE'],
|
|
||||||
'🇸🇬': ['SG', 'SGP'],
|
|
||||||
'🇸🇮': ['SI', 'SVN'],
|
|
||||||
'🇸🇰': ['SK', 'SVK'],
|
|
||||||
'🇹🇭': ['TH', 'THA'],
|
|
||||||
'🇹🇳': ['TN', 'TUN'],
|
|
||||||
'🇹🇷': ['TR', 'TUR'],
|
|
||||||
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET'],
|
|
||||||
'🇺🇦': ['UA', 'UKR'],
|
|
||||||
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
|
|
||||||
'🇺🇾': ['UY', 'URY'],
|
|
||||||
'🇻🇪': ['VE', 'VEN'],
|
|
||||||
'🇻🇳': ['VN', 'VNM'],
|
|
||||||
'🇿🇦': ['ZA', 'ZAF'],
|
|
||||||
'🇨🇳': ['CN', 'CHN', 'BACK'],
|
|
||||||
};
|
|
||||||
// 原旗帜或空
|
// 原旗帜或空
|
||||||
let Flag =
|
let Flag =
|
||||||
name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
|
name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
|
||||||
@@ -393,7 +436,9 @@ export function getFlag(name) {
|
|||||||
// 不精确匹配(只要包含就算,忽略大小写)
|
// 不精确匹配(只要包含就算,忽略大小写)
|
||||||
keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
|
keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
|
||||||
) {
|
) {
|
||||||
//console.log(`newFlag = ${flag}`)
|
if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {
|
||||||
|
return (Flag = '🇨🇳');
|
||||||
|
}
|
||||||
return (Flag = flag);
|
return (Flag = flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,10 +452,67 @@ export function getFlag(name) {
|
|||||||
RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
|
RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
//console.log(`ISOFlag = ${flag}`)
|
const isCN2 =
|
||||||
return (Flag = flag);
|
flag == '🇨🇳' &&
|
||||||
|
RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);
|
||||||
|
if (!isCN2) {
|
||||||
|
return (Flag = flag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log(`Final Flag = ${Flag}`)
|
//console.log(`Final Flag = ${Flag}`)
|
||||||
return Flag;
|
return Flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getISO(name) {
|
||||||
|
return ISOFlags[getFlag(name)]?.[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove flag
|
||||||
|
export function removeFlag(str) {
|
||||||
|
return str
|
||||||
|
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MMDB {
|
||||||
|
constructor({ country, asn } = {}) {
|
||||||
|
if ($.env.isNode) {
|
||||||
|
const Reader = eval(`require("@maxmind/geoip2-node")`).Reader;
|
||||||
|
const fs = eval("require('fs')");
|
||||||
|
const countryFile =
|
||||||
|
country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
|
||||||
|
const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH');
|
||||||
|
// $.info(
|
||||||
|
// `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync(
|
||||||
|
// countryFile,
|
||||||
|
// )}`,
|
||||||
|
// );
|
||||||
|
if (countryFile) {
|
||||||
|
this.countryReader = Reader.openBuffer(
|
||||||
|
fs.readFileSync(countryFile),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// $.info(
|
||||||
|
// `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync(
|
||||||
|
// asnFile,
|
||||||
|
// )}`,
|
||||||
|
// );
|
||||||
|
if (asnFile) {
|
||||||
|
if (!fs.existsSync(asnFile))
|
||||||
|
throw new Error('GeoLite2 ASN MMDB does not exist');
|
||||||
|
this.asnReader = Reader.openBuffer(fs.readFileSync(asnFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
geoip(ip) {
|
||||||
|
return this.countryReader?.country(ip)?.country?.isoCode;
|
||||||
|
}
|
||||||
|
ipaso(ip) {
|
||||||
|
return this.asnReader?.asn(ip)?.autonomousSystemOrganization;
|
||||||
|
}
|
||||||
|
ipasn(ip) {
|
||||||
|
return this.asnReader?.asn(ip)?.autonomousSystemNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,84 +1,286 @@
|
|||||||
import { HTTP } from '@/vendor/open-api';
|
import { HTTP, ENV } from '@/vendor/open-api';
|
||||||
|
import { getPolicyDescriptor } from '@/utils';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
import { SETTINGS_KEY } from '@/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gist backup
|
* Gist backup
|
||||||
*/
|
*/
|
||||||
export default class Gist {
|
export default class Gist {
|
||||||
constructor({ token, key }) {
|
constructor({ token, key, syncPlatform }) {
|
||||||
this.http = HTTP({
|
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||||
baseURL: 'https://api.github.com',
|
const { defaultProxy, defaultTimeout: timeout } = $.read(SETTINGS_KEY);
|
||||||
headers: {
|
let proxy = defaultProxy;
|
||||||
|
if ($.env.isNode) {
|
||||||
|
proxy =
|
||||||
|
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncPlatform === 'gitlab') {
|
||||||
|
this.headers = {
|
||||||
|
'PRIVATE-TOKEN': `${token}`,
|
||||||
|
'User-Agent':
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||||
|
};
|
||||||
|
this.http = HTTP({
|
||||||
|
baseURL: 'https://gitlab.com/api/v4',
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
...(isStash && proxy
|
||||||
|
? {
|
||||||
|
'X-Stash-Selected-Proxy':
|
||||||
|
encodeURIComponent(proxy),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(isShadowRocket && proxy
|
||||||
|
? { 'X-Surge-Policy': proxy }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
timeout: timeout || 8000,
|
||||||
|
|
||||||
|
events: {
|
||||||
|
onResponse: (resp) => {
|
||||||
|
if (/^[45]/.test(String(resp.statusCode))) {
|
||||||
|
const body = JSON.parse(resp.body);
|
||||||
|
return Promise.reject(
|
||||||
|
`ERROR: ${body.message?.error ?? body.message}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.headers = {
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
|
||||||
},
|
};
|
||||||
events: {
|
this.http = HTTP({
|
||||||
onResponse: (resp) => {
|
baseURL: 'https://api.github.com',
|
||||||
if (/^[45]/.test(String(resp.statusCode))) {
|
headers: {
|
||||||
return Promise.reject(
|
...this.headers,
|
||||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
...(isStash && proxy
|
||||||
);
|
? {
|
||||||
} else {
|
'X-Stash-Selected-Proxy':
|
||||||
return resp;
|
encodeURIComponent(proxy),
|
||||||
}
|
}
|
||||||
|
: {}),
|
||||||
|
...(isShadowRocket && proxy
|
||||||
|
? { 'X-Surge-Policy': proxy }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
timeout: timeout || 8000,
|
||||||
|
|
||||||
|
events: {
|
||||||
|
onResponse: (resp) => {
|
||||||
|
if (/^[45]/.test(String(resp.statusCode))) {
|
||||||
|
return Promise.reject(
|
||||||
|
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
this.syncPlatform = syncPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
async locate() {
|
async locate() {
|
||||||
return this.http.get('/gists').then((response) => {
|
if (this.syncPlatform === 'gitlab') {
|
||||||
const gists = JSON.parse(response.body);
|
return this.http.get('/snippets').then((response) => {
|
||||||
for (let g of gists) {
|
const gists = JSON.parse(response.body);
|
||||||
if (g.description === this.key) {
|
|
||||||
return g.id;
|
for (let g of gists) {
|
||||||
|
if (g.title === this.key) {
|
||||||
|
return g;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return;
|
||||||
return -1;
|
});
|
||||||
});
|
} else {
|
||||||
|
return this.http
|
||||||
|
.get('/gists?per_page=100&page=1')
|
||||||
|
.then((response) => {
|
||||||
|
const gists = JSON.parse(response.body);
|
||||||
|
$.info(`获取到当前 GitHub 用户的 gist: ${gists.length} 个`);
|
||||||
|
for (let g of gists) {
|
||||||
|
if (g.description === this.key) {
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload(files) {
|
async upload(input) {
|
||||||
if (Object.keys(files).length === 0) {
|
if (Object.keys(input).length === 0) {
|
||||||
return Promise.reject('未提供需上传的文件');
|
return Promise.reject('未提供需上传的文件');
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = await this.locate();
|
const gist = await this.locate();
|
||||||
|
|
||||||
if (id === -1) {
|
let files = input;
|
||||||
// create a new gist for backup
|
|
||||||
return this.http.post({
|
if (gist?.id) {
|
||||||
url: '/gists',
|
if (this.syncPlatform === 'gitlab') {
|
||||||
body: JSON.stringify({
|
gist.files = gist.files.reduce((acc, item) => {
|
||||||
description: this.key,
|
acc[item.path] = item;
|
||||||
public: false,
|
return acc;
|
||||||
files,
|
}, {});
|
||||||
}),
|
}
|
||||||
|
// console.log(`files`, files);
|
||||||
|
// console.log(`gist`, gist.files);
|
||||||
|
let actions = [];
|
||||||
|
const result = { ...gist.files };
|
||||||
|
Object.keys(files).map((key) => {
|
||||||
|
if (result[key]) {
|
||||||
|
if (
|
||||||
|
files[key].content == null ||
|
||||||
|
files[key].content === ''
|
||||||
|
) {
|
||||||
|
delete result[key];
|
||||||
|
actions.push({
|
||||||
|
action: 'delete',
|
||||||
|
file_path: key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[key] = files[key];
|
||||||
|
actions.push({
|
||||||
|
action: 'update',
|
||||||
|
file_path: key,
|
||||||
|
content: files[key].content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
files[key].content == null ||
|
||||||
|
files[key].content === ''
|
||||||
|
) {
|
||||||
|
delete result[key];
|
||||||
|
delete files[key];
|
||||||
|
} else {
|
||||||
|
result[key] = files[key];
|
||||||
|
actions.push({
|
||||||
|
action: 'create',
|
||||||
|
file_path: key,
|
||||||
|
content: files[key].content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// console.log(`result`, result);
|
||||||
|
// console.log(`files`, files);
|
||||||
|
// console.log(`actions`, actions);
|
||||||
|
|
||||||
|
if (this.syncPlatform === 'gitlab') {
|
||||||
|
if (Object.keys(result).length === 0) {
|
||||||
|
return Promise.reject(
|
||||||
|
'本次操作将导致所有文件的内容都为空, 无法更新 snippet',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.keys(result).length > 10) {
|
||||||
|
return Promise.reject(
|
||||||
|
'本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
files = actions;
|
||||||
|
return this.http.put({
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
url: `/snippets/${gist.id}`,
|
||||||
|
body: JSON.stringify({ files }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (Object.keys(result).length === 0) {
|
||||||
|
return Promise.reject(
|
||||||
|
'本次操作将导致所有文件的内容都为空, 无法更新 gist',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.http.patch({
|
||||||
|
url: `/gists/${gist.id}`,
|
||||||
|
body: JSON.stringify({ files }),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// update an existing gist
|
files = Object.entries(files).reduce((acc, [key, file]) => {
|
||||||
return this.http.patch({
|
if (file.content !== null && file.content !== '') {
|
||||||
url: `/gists/${id}`,
|
acc[key] = file;
|
||||||
body: JSON.stringify({ files }),
|
}
|
||||||
});
|
return acc;
|
||||||
|
}, {});
|
||||||
|
if (this.syncPlatform === 'gitlab') {
|
||||||
|
if (Object.keys(files).length === 0) {
|
||||||
|
return Promise.reject(
|
||||||
|
'所有文件的内容都为空, 无法创建 snippet',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
files = Object.keys(files).map((key) => ({
|
||||||
|
file_path: key,
|
||||||
|
content: files[key].content,
|
||||||
|
}));
|
||||||
|
return this.http.post({
|
||||||
|
headers: {
|
||||||
|
...this.headers,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
url: '/snippets',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: this.key,
|
||||||
|
visibility: 'private',
|
||||||
|
files,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (Object.keys(files).length === 0) {
|
||||||
|
return Promise.reject(
|
||||||
|
'所有文件的内容都为空, 无法创建 gist',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.http.post({
|
||||||
|
url: '/gists',
|
||||||
|
body: JSON.stringify({
|
||||||
|
description: this.key,
|
||||||
|
public: false,
|
||||||
|
files,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async download(filename) {
|
async download(filename) {
|
||||||
const id = await this.locate();
|
const gist = await this.locate();
|
||||||
if (id === -1) {
|
if (gist?.id) {
|
||||||
return Promise.reject('未找到Gist备份!');
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
const { files } = await this.http
|
const { files } = await this.http
|
||||||
.get(`/gists/${id}`)
|
.get(`/gists/${gist.id}`)
|
||||||
.then((resp) => JSON.parse(resp.body));
|
.then((resp) => JSON.parse(resp.body));
|
||||||
const url = files[filename].raw_url;
|
const url = files[filename].raw_url;
|
||||||
return await this.http.get(url).then((resp) => resp.body);
|
return await this.http.get(url).then((resp) => resp.body);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.reject('找不到 Sub-Store Gist');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
backend/src/utils/headers-resource-cache.js
Normal file
117
backend/src/utils/headers-resource-cache.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import $ from '@/core/app';
|
||||||
|
import {
|
||||||
|
HEADERS_RESOURCE_CACHE_KEY,
|
||||||
|
CHR_EXPIRATION_TIME_KEY,
|
||||||
|
} from '@/constants';
|
||||||
|
|
||||||
|
class ResourceCache {
|
||||||
|
constructor() {
|
||||||
|
this.expires = getExpiredTime();
|
||||||
|
if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
|
||||||
|
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
|
||||||
|
e?.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
this.resourceCache = {};
|
||||||
|
$.write('{}', HEADERS_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), HEADERS_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(CHR_EXPIRATION_TIME_KEY));
|
||||||
|
if (!$.read(CHR_EXPIRATION_TIME_KEY)) {
|
||||||
|
$.write('6e4', CHR_EXPIRATION_TIME_KEY); // 1分钟
|
||||||
|
}
|
||||||
|
let expiration = 6e4;
|
||||||
|
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(
|
||||||
|
'#\u54cd\u5e94\u5934\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(CHR_EXPIRATION_TIME_KEY);
|
||||||
|
return expiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ResourceCache();
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as ipAddress from 'ip-address';
|
||||||
// source: https://stackoverflow.com/a/36760050
|
// source: https://stackoverflow.com/a/36760050
|
||||||
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
|
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
|
||||||
|
|
||||||
@@ -13,6 +14,12 @@ function isIPv6(ip) {
|
|||||||
return IPV6_REGEX.test(ip);
|
return IPV6_REGEX.test(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidPortNumber(port) {
|
||||||
|
return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isNotBlank(str) {
|
function isNotBlank(str) {
|
||||||
return typeof str === 'string' && str.trim().length > 0;
|
return typeof str === 'string' && str.trim().length > 0;
|
||||||
}
|
}
|
||||||
@@ -29,4 +36,108 @@ function getIfPresent(obj, defaultValue) {
|
|||||||
return isPresent(obj) ? obj : defaultValue;
|
return isPresent(obj) ? obj : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent };
|
function getPolicyDescriptor(str) {
|
||||||
|
if (!str) return {};
|
||||||
|
return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str)
|
||||||
|
? {
|
||||||
|
'policy-descriptor': str,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
policy: str,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// const utf8ArrayToStr =
|
||||||
|
// typeof TextDecoder !== 'undefined'
|
||||||
|
// ? (v) => new TextDecoder().decode(new Uint8Array(v))
|
||||||
|
// : (function () {
|
||||||
|
// var charCache = new Array(128); // Preallocate the cache for the common single byte chars
|
||||||
|
// var charFromCodePt = String.fromCodePoint || String.fromCharCode;
|
||||||
|
// var result = [];
|
||||||
|
|
||||||
|
// return function (array) {
|
||||||
|
// var codePt, byte1;
|
||||||
|
// var buffLen = array.length;
|
||||||
|
|
||||||
|
// result.length = 0;
|
||||||
|
|
||||||
|
// for (var i = 0; i < buffLen; ) {
|
||||||
|
// byte1 = array[i++];
|
||||||
|
|
||||||
|
// if (byte1 <= 0x7f) {
|
||||||
|
// codePt = byte1;
|
||||||
|
// } else if (byte1 <= 0xdf) {
|
||||||
|
// codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
|
||||||
|
// } else if (byte1 <= 0xef) {
|
||||||
|
// codePt =
|
||||||
|
// ((byte1 & 0x0f) << 12) |
|
||||||
|
// ((array[i++] & 0x3f) << 6) |
|
||||||
|
// (array[i++] & 0x3f);
|
||||||
|
// } else if (String.fromCodePoint) {
|
||||||
|
// codePt =
|
||||||
|
// ((byte1 & 0x07) << 18) |
|
||||||
|
// ((array[i++] & 0x3f) << 12) |
|
||||||
|
// ((array[i++] & 0x3f) << 6) |
|
||||||
|
// (array[i++] & 0x3f);
|
||||||
|
// } else {
|
||||||
|
// codePt = 63; // Cannot convert four byte code points, so use "?" instead
|
||||||
|
// i += 3;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result.push(
|
||||||
|
// charCache[codePt] ||
|
||||||
|
// (charCache[codePt] = charFromCodePt(codePt)),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return result.join('');
|
||||||
|
// };
|
||||||
|
// })();
|
||||||
|
|
||||||
|
function getRandomInt(min, max) {
|
||||||
|
min = Math.ceil(min);
|
||||||
|
max = Math.floor(max);
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomPort(portString) {
|
||||||
|
let portParts = portString.split(/,|\//);
|
||||||
|
let randomPart = portParts[Math.floor(Math.random() * portParts.length)];
|
||||||
|
if (randomPart.includes('-')) {
|
||||||
|
let [min, max] = randomPart.split('-').map(Number);
|
||||||
|
return getRandomInt(min, max);
|
||||||
|
} else {
|
||||||
|
return Number(randomPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberToString(value) {
|
||||||
|
return Number.isSafeInteger(value)
|
||||||
|
? String(value)
|
||||||
|
: BigInt(value).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUUID(uuid) {
|
||||||
|
return (
|
||||||
|
typeof uuid === 'string' &&
|
||||||
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
||||||
|
uuid,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
isValidUUID,
|
||||||
|
ipAddress,
|
||||||
|
isIPv4,
|
||||||
|
isIPv6,
|
||||||
|
isValidPortNumber,
|
||||||
|
isNotBlank,
|
||||||
|
getIfNotBlank,
|
||||||
|
isPresent,
|
||||||
|
getIfPresent,
|
||||||
|
// utf8ArrayToStr,
|
||||||
|
getPolicyDescriptor,
|
||||||
|
getRandomPort,
|
||||||
|
numberToString,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
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 '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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,17 @@ class ResourceCache {
|
|||||||
if (!$.read(RESOURCE_CACHE_KEY)) {
|
if (!$.read(RESOURCE_CACHE_KEY)) {
|
||||||
$.write('{}', RESOURCE_CACHE_KEY);
|
$.write('{}', RESOURCE_CACHE_KEY);
|
||||||
}
|
}
|
||||||
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
|
try {
|
||||||
|
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
|
||||||
|
e?.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
this.resourceCache = {};
|
||||||
|
$.write('{}', RESOURCE_CACHE_KEY);
|
||||||
|
}
|
||||||
this._cleanup();
|
this._cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
backend/src/utils/rs.js
Normal file
11
backend/src/utils/rs.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import rs from 'jsrsasign';
|
||||||
|
|
||||||
|
export function generateFingerprint(caStr) {
|
||||||
|
const hex = rs.pemtohex(caStr);
|
||||||
|
const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256');
|
||||||
|
return fingerPrint.match(/.{2}/g).join(':').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
generateFingerprint,
|
||||||
|
};
|
||||||
@@ -10,11 +10,21 @@ class ResourceCache {
|
|||||||
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
||||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||||
}
|
}
|
||||||
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
|
try {
|
||||||
|
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`解析持久化缓存中的 ${SCRIPT_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
|
||||||
|
e?.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
this.resourceCache = {};
|
||||||
|
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||||
|
}
|
||||||
this._cleanup();
|
this._cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanup() {
|
_cleanup(prefix, expires) {
|
||||||
// clear obsolete cached resource
|
// clear obsolete cached resource
|
||||||
let clear = false;
|
let clear = false;
|
||||||
Object.entries(this.resourceCache).forEach((entry) => {
|
Object.entries(this.resourceCache).forEach((entry) => {
|
||||||
@@ -25,7 +35,11 @@ class ResourceCache {
|
|||||||
$.delete(`#${id}`);
|
$.delete(`#${id}`);
|
||||||
clear = true;
|
clear = true;
|
||||||
}
|
}
|
||||||
if (new Date().getTime() - updated.time > this.expires) {
|
if (
|
||||||
|
new Date().getTime() - updated.time >
|
||||||
|
(expires ?? this.expires) ||
|
||||||
|
(prefix && id.startsWith(prefix))
|
||||||
|
) {
|
||||||
delete this.resourceCache[id];
|
delete this.resourceCache[id];
|
||||||
clear = true;
|
clear = true;
|
||||||
}
|
}
|
||||||
@@ -42,10 +56,15 @@ class ResourceCache {
|
|||||||
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
|
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id, expires, remove) {
|
||||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
if (updated) {
|
||||||
return this.resourceCache[id].data;
|
if (new Date().getTime() - updated <= (expires ?? this.expires))
|
||||||
|
return this.resourceCache[id].data;
|
||||||
|
if (remove) {
|
||||||
|
delete this.resourceCache[id];
|
||||||
|
this._persist();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
91
backend/src/utils/user-agent.js
Normal file
91
backend/src/utils/user-agent.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import gte from 'semver/functions/gte';
|
||||||
|
import coerce from 'semver/functions/coerce';
|
||||||
|
import $ from '@/core/app';
|
||||||
|
|
||||||
|
export function getUserAgentFromHeaders(headers) {
|
||||||
|
const keys = Object.keys(headers);
|
||||||
|
let UA = '';
|
||||||
|
let ua = '';
|
||||||
|
let accept = '';
|
||||||
|
for (let k of keys) {
|
||||||
|
const lower = k.toLowerCase();
|
||||||
|
if (lower === 'user-agent') {
|
||||||
|
UA = headers[k];
|
||||||
|
ua = UA.toLowerCase();
|
||||||
|
} else if (lower === 'accept') {
|
||||||
|
accept = headers[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { UA, ua, accept };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatformFromUserAgent({ ua, UA, accept }) {
|
||||||
|
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||||
|
return 'QX';
|
||||||
|
} else if (ua.indexOf('egern') !== -1) {
|
||||||
|
return 'Egern';
|
||||||
|
} else if (UA.indexOf('Surfboard') !== -1) {
|
||||||
|
return 'Surfboard';
|
||||||
|
} 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 'Shadowrocket';
|
||||||
|
} else if (UA.indexOf('Stash') !== -1) {
|
||||||
|
return 'Stash';
|
||||||
|
} else if (
|
||||||
|
ua === 'meta' ||
|
||||||
|
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) ||
|
||||||
|
ua.indexOf('clash-verge') !== -1 ||
|
||||||
|
ua.indexOf('flclash') !== -1
|
||||||
|
) {
|
||||||
|
return 'ClashMeta';
|
||||||
|
} else if (ua.indexOf('clash') !== -1) {
|
||||||
|
return 'Clash';
|
||||||
|
} else if (ua.indexOf('v2ray') !== -1) {
|
||||||
|
return 'V2Ray';
|
||||||
|
} else if (ua.indexOf('sing-box') !== -1) {
|
||||||
|
return 'sing-box';
|
||||||
|
} else if (accept.indexOf('application/json') === 0) {
|
||||||
|
return 'JSON';
|
||||||
|
} else {
|
||||||
|
return 'V2Ray';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatformFromHeaders(headers) {
|
||||||
|
const { UA, ua, accept } = getUserAgentFromHeaders(headers);
|
||||||
|
return getPlatformFromUserAgent({ ua, UA, accept });
|
||||||
|
}
|
||||||
|
export function shouldIncludeUnsupportedProxy(platform, ua) {
|
||||||
|
try {
|
||||||
|
const target = getPlatformFromUserAgent({
|
||||||
|
UA: ua,
|
||||||
|
ua: ua.toLowerCase(),
|
||||||
|
});
|
||||||
|
if (!['Stash', 'Egern'].includes(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const version = coerce(ua).version;
|
||||||
|
if (
|
||||||
|
platform === 'Stash' &&
|
||||||
|
target === 'Stash' &&
|
||||||
|
gte(version, '2.8.0')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
platform === 'Egern' &&
|
||||||
|
target === 'Egern' &&
|
||||||
|
gte(version, '1.29.0')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.error(`获取版本号失败: ${e}`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
37
backend/src/utils/yaml.js
Normal file
37
backend/src/utils/yaml.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import YAML from 'static-js-yaml';
|
||||||
|
|
||||||
|
function retry(fn, content, ...args) {
|
||||||
|
try {
|
||||||
|
return fn(content, ...args);
|
||||||
|
} catch (e) {
|
||||||
|
return fn(
|
||||||
|
dump(
|
||||||
|
fn(
|
||||||
|
content.replace(/!<str>\s*/g, '__SubStoreJSYAMLString__'),
|
||||||
|
...args,
|
||||||
|
),
|
||||||
|
).replace(/__SubStoreJSYAMLString__/g, ''),
|
||||||
|
...args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeLoad(content, ...args) {
|
||||||
|
return retry(YAML.safeLoad, JSON.parse(JSON.stringify(content)), ...args);
|
||||||
|
}
|
||||||
|
export function load(content, ...args) {
|
||||||
|
return retry(YAML.load, JSON.parse(JSON.stringify(content)), ...args);
|
||||||
|
}
|
||||||
|
export function safeDump(content, ...args) {
|
||||||
|
return YAML.safeDump(JSON.parse(JSON.stringify(content)), ...args);
|
||||||
|
}
|
||||||
|
export function dump(content, ...args) {
|
||||||
|
return YAML.dump(JSON.parse(JSON.stringify(content)), ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
safeLoad,
|
||||||
|
load,
|
||||||
|
safeDump,
|
||||||
|
dump,
|
||||||
|
};
|
||||||
7
backend/src/vendor/express.js
vendored
7
backend/src/vendor/express.js
vendored
@@ -2,8 +2,6 @@
|
|||||||
import { ENV } from './open-api';
|
import { ENV } from './open-api';
|
||||||
|
|
||||||
export default function express({ substore: $, port, host }) {
|
export default function express({ substore: $, port, host }) {
|
||||||
port = port || 3000;
|
|
||||||
host = host || '::';
|
|
||||||
const { isNode } = ENV();
|
const { isNode } = ENV();
|
||||||
const DEFAULT_HEADERS = {
|
const DEFAULT_HEADERS = {
|
||||||
'Content-Type': 'text/plain;charset=UTF-8',
|
'Content-Type': 'text/plain;charset=UTF-8',
|
||||||
@@ -11,6 +9,7 @@ export default function express({ substore: $, port, host }) {
|
|||||||
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
|
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
|
||||||
'Access-Control-Allow-Headers':
|
'Access-Control-Allow-Headers':
|
||||||
'Origin, X-Requested-With, Content-Type, Accept',
|
'Origin, X-Requested-With, Content-Type, Accept',
|
||||||
|
'X-Powered-By': 'Sub-Store',
|
||||||
};
|
};
|
||||||
|
|
||||||
// node support
|
// node support
|
||||||
@@ -163,7 +162,7 @@ export default function express({ substore: $, port, host }) {
|
|||||||
|
|
||||||
function Response() {
|
function Response() {
|
||||||
let statusCode = 200;
|
let statusCode = 200;
|
||||||
const { isQX, isLoon, isSurge } = ENV();
|
const { isQX, isLoon, isSurge, isGUIforCores } = ENV();
|
||||||
const headers = DEFAULT_HEADERS;
|
const headers = DEFAULT_HEADERS;
|
||||||
const STATUS_CODE_MAP = {
|
const STATUS_CODE_MAP = {
|
||||||
200: 'HTTP/1.1 200 OK',
|
200: 'HTTP/1.1 200 OK',
|
||||||
@@ -186,7 +185,7 @@ export default function express({ substore: $, port, host }) {
|
|||||||
body,
|
body,
|
||||||
headers,
|
headers,
|
||||||
};
|
};
|
||||||
if (isQX) {
|
if (isQX || isGUIforCores) {
|
||||||
$done(response);
|
$done(response);
|
||||||
} else if (isLoon || isSurge) {
|
} else if (isLoon || isSurge) {
|
||||||
$done({
|
$done({
|
||||||
|
|||||||
138
backend/src/vendor/open-api.js
vendored
138
backend/src/vendor/open-api.js
vendored
@@ -6,6 +6,9 @@ const isNode = eval(`typeof process !== "undefined"`); // eval is needed in orde
|
|||||||
const isStash =
|
const isStash =
|
||||||
'undefined' !== typeof $environment && $environment['stash-version'];
|
'undefined' !== typeof $environment && $environment['stash-version'];
|
||||||
const isShadowRocket = 'undefined' !== typeof $rocket;
|
const isShadowRocket = 'undefined' !== typeof $rocket;
|
||||||
|
const isEgern = 'object' == typeof egern;
|
||||||
|
const isLanceX = 'undefined' != typeof $native;
|
||||||
|
const isGUIforCores = typeof $Plugins !== 'undefined';
|
||||||
|
|
||||||
export class OpenAPI {
|
export class OpenAPI {
|
||||||
constructor(name = 'untitled', debug = false) {
|
constructor(name = 'untitled', debug = false) {
|
||||||
@@ -46,7 +49,10 @@ export class OpenAPI {
|
|||||||
this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
|
this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
|
||||||
if (isLoon || isSurge)
|
if (isLoon || isSurge)
|
||||||
this.cache = JSON.parse($persistentStore.read(this.name) || '{}');
|
this.cache = JSON.parse($persistentStore.read(this.name) || '{}');
|
||||||
|
if (isGUIforCores)
|
||||||
|
this.cache = JSON.parse(
|
||||||
|
$Plugins.SubStoreCache.get(this.name) || '{}',
|
||||||
|
);
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
// create a json for root cache
|
// create a json for root cache
|
||||||
const basePath =
|
const basePath =
|
||||||
@@ -84,6 +90,7 @@ export class OpenAPI {
|
|||||||
const data = JSON.stringify(this.cache, null, 2);
|
const data = JSON.stringify(this.cache, null, 2);
|
||||||
if (isQX) $prefs.setValueForKey(data, this.name);
|
if (isQX) $prefs.setValueForKey(data, this.name);
|
||||||
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
||||||
|
if (isGUIforCores) $Plugins.SubStoreCache.set(this.name, data);
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
const basePath =
|
const basePath =
|
||||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||||
@@ -116,6 +123,9 @@ export class OpenAPI {
|
|||||||
if (isNode) {
|
if (isNode) {
|
||||||
this.root[key] = data;
|
this.root[key] = data;
|
||||||
}
|
}
|
||||||
|
if (isGUIforCores) {
|
||||||
|
return $Plugins.SubStoreCache.set(key, data);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.cache[key] = data;
|
this.cache[key] = data;
|
||||||
}
|
}
|
||||||
@@ -135,6 +145,9 @@ export class OpenAPI {
|
|||||||
if (isNode) {
|
if (isNode) {
|
||||||
return this.root[key];
|
return this.root[key];
|
||||||
}
|
}
|
||||||
|
if (isGUIforCores) {
|
||||||
|
return $Plugins.SubStoreCache.get(key);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.cache[key];
|
return this.cache[key];
|
||||||
}
|
}
|
||||||
@@ -153,6 +166,9 @@ export class OpenAPI {
|
|||||||
if (isNode) {
|
if (isNode) {
|
||||||
delete this.root[key];
|
delete this.root[key];
|
||||||
}
|
}
|
||||||
|
if (isGUIforCores) {
|
||||||
|
return $Plugins.SubStoreCache.remove(key);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete this.cache[key];
|
delete this.cache[key];
|
||||||
}
|
}
|
||||||
@@ -191,6 +207,35 @@ export class OpenAPI {
|
|||||||
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
||||||
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
||||||
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
||||||
|
|
||||||
|
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
|
||||||
|
if (push) {
|
||||||
|
const url = push
|
||||||
|
.replace(
|
||||||
|
'[推送标题]',
|
||||||
|
encodeURIComponent(title || 'Sub-Store'),
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
'[推送内容]',
|
||||||
|
encodeURIComponent(
|
||||||
|
[subtitle, content_].map((i) => i).join('\n'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const $http = HTTP();
|
||||||
|
$http
|
||||||
|
.get({ url })
|
||||||
|
.then((resp) => {
|
||||||
|
console.log(
|
||||||
|
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isGUIforCores) {
|
||||||
|
$Plugins.Notify(title, subtitle + '\n' + content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +257,7 @@ export class OpenAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
done(value = {}) {
|
done(value = {}) {
|
||||||
if (isQX || isLoon || isSurge) {
|
if (isQX || isLoon || isSurge || isGUIforCores) {
|
||||||
$done(value);
|
$done(value);
|
||||||
} else if (isNode) {
|
} else if (isNode) {
|
||||||
if (typeof $context !== 'undefined') {
|
if (typeof $context !== 'undefined') {
|
||||||
@@ -225,11 +270,21 @@ export class OpenAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ENV() {
|
export function ENV() {
|
||||||
return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket };
|
return {
|
||||||
|
isQX,
|
||||||
|
isLoon,
|
||||||
|
isSurge,
|
||||||
|
isNode,
|
||||||
|
isStash,
|
||||||
|
isShadowRocket,
|
||||||
|
isEgern,
|
||||||
|
isLanceX,
|
||||||
|
isGUIforCores,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HTTP(defaultOptions = { baseURL: '' }) {
|
export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||||
const { isQX, isLoon, isSurge, isNode } = ENV();
|
const { isQX, isLoon, isSurge, isNode, isGUIforCores } = ENV();
|
||||||
const methods = [
|
const methods = [
|
||||||
'GET',
|
'GET',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -279,31 +334,80 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
|||||||
url: options.url,
|
url: options.url,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
body: options.body,
|
body: options.body,
|
||||||
|
opts: options.opts,
|
||||||
});
|
});
|
||||||
} else if (isLoon || isSurge || isNode) {
|
} else if (isLoon || isSurge || isNode) {
|
||||||
worker = new Promise((resolve, reject) => {
|
worker = new Promise((resolve, reject) => {
|
||||||
const request = isNode
|
const request = isNode
|
||||||
? eval("require('request')")
|
? eval("require('request')")
|
||||||
: $httpClient;
|
: $httpClient;
|
||||||
request[method.toLowerCase()](
|
const body = options.body;
|
||||||
options,
|
const opts = JSON.parse(JSON.stringify(options));
|
||||||
(err, response, body) => {
|
opts.body = body;
|
||||||
if (err) reject(err);
|
|
||||||
else
|
if (!isNode && opts.timeout) {
|
||||||
resolve({
|
opts.timeout++;
|
||||||
statusCode:
|
let unit = 'ms';
|
||||||
response.status || response.statusCode,
|
// 这些客户端单位为 s
|
||||||
headers: response.headers,
|
if (isSurge || isStash || isShadowRocket) {
|
||||||
body,
|
opts.timeout = Math.ceil(opts.timeout / 1000);
|
||||||
});
|
unit = 's';
|
||||||
},
|
}
|
||||||
);
|
// Loon 为 ms
|
||||||
|
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
|
||||||
|
}
|
||||||
|
request[method.toLowerCase()](opts, (err, response, body) => {
|
||||||
|
// if (err) {
|
||||||
|
// console.log(err);
|
||||||
|
// } else {
|
||||||
|
// console.log({
|
||||||
|
// statusCode:
|
||||||
|
// response.status || response.statusCode,
|
||||||
|
// headers: response.headers,
|
||||||
|
// body,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (err) reject(err);
|
||||||
|
else
|
||||||
|
resolve({
|
||||||
|
statusCode: response.status || response.statusCode,
|
||||||
|
headers: response.headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (isGUIforCores) {
|
||||||
|
worker = new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const response = await $Plugins.Requests({
|
||||||
|
method,
|
||||||
|
url: options.url,
|
||||||
|
headers: options.headers,
|
||||||
|
body: options.body,
|
||||||
|
options: {
|
||||||
|
Proxy: options.proxy,
|
||||||
|
Timeout: options.timeout
|
||||||
|
? options.timeout / 1000
|
||||||
|
: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resolve({
|
||||||
|
statusCode: response.status,
|
||||||
|
headers: response.headers,
|
||||||
|
body: response.body,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutid;
|
let timeoutid;
|
||||||
|
|
||||||
const timer = timeout
|
const timer = timeout
|
||||||
? new Promise((_, reject) => {
|
? new Promise((_, reject) => {
|
||||||
|
// console.log(`[request timeout] ${timeout}ms`);
|
||||||
timeoutid = setTimeout(() => {
|
timeoutid = setTimeout(() => {
|
||||||
events.onTimeout();
|
events.onTimeout();
|
||||||
return reject(
|
return reject(
|
||||||
|
|||||||
38
config/Egern.yaml
Normal file
38
config/Egern.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Sub-Store
|
||||||
|
description: '支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *'
|
||||||
|
compat_arguments:
|
||||||
|
ability: http-client-policy
|
||||||
|
cronexp: 55 23 * * *
|
||||||
|
sync: '"Sub-Store Sync"'
|
||||||
|
timeout: '120'
|
||||||
|
engine: auto
|
||||||
|
produce: '"# Sub-Store Produce"'
|
||||||
|
produce_cronexp: 50 */6 * * *
|
||||||
|
produce_sub: '"sub1,sub2"'
|
||||||
|
produce_col: '"col1,col2"'
|
||||||
|
compat_arguments_desc: '\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
|
||||||
|
scriptings:
|
||||||
|
- http_request:
|
||||||
|
name: Sub-Store Core
|
||||||
|
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
|
||||||
|
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
|
||||||
|
body_required: true
|
||||||
|
- http_request:
|
||||||
|
name: Sub-Store Simple
|
||||||
|
match: ^https?:\/\/sub\.store
|
||||||
|
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
|
||||||
|
body_required: true
|
||||||
|
- schedule:
|
||||||
|
name: '{{{sync}}}'
|
||||||
|
cron: '{{{cronexp}}}'
|
||||||
|
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
|
||||||
|
- schedule:
|
||||||
|
name: '{{{produce}}}'
|
||||||
|
cron: '{{{produce_cronexp}}}'
|
||||||
|
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
|
||||||
|
arguments:
|
||||||
|
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
|
||||||
|
mitm:
|
||||||
|
hostnames:
|
||||||
|
includes:
|
||||||
|
- sub.store
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
#!name=Sub-Store
|
#!name=Sub-Store
|
||||||
#!desc=高级订阅管理工具
|
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
|
||||||
#!openUrl=https://sub.store
|
#!openUrl=https://sub.store
|
||||||
#!author=Peng-YM
|
#!author=Peng-YM
|
||||||
#!homepage=https://github.com/sub-store-org/Sub-Store
|
#!homepage=https://github.com/sub-store-org/Sub-Store
|
||||||
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
|
||||||
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||||
|
#!select = 响应头缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
|
||||||
|
|
||||||
[Rule]
|
[Rule]
|
||||||
DOMAIN,sub-store.vercel.app,PROXY
|
DOMAIN,sub-store.vercel.app,PROXY
|
||||||
@@ -13,7 +14,7 @@ DOMAIN,sub-store.vercel.app,PROXY
|
|||||||
hostname=sub.store
|
hostname=sub.store
|
||||||
|
|
||||||
[Script]
|
[Script]
|
||||||
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\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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
|
http-request ^https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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
|
cron "55 23 * * *" script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name":"Sub-Store",
|
"name": "Sub-Store",
|
||||||
"description":"",
|
"description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'",
|
||||||
"task":[
|
"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"
|
"55 23 * * * https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
hostname=sub.store
|
hostname=sub.store
|
||||||
|
|
||||||
^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\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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
|
^https?:\/\/sub\.store url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
|
||||||
@@ -6,29 +6,57 @@ Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](http
|
|||||||
|
|
||||||
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||||
|
|
||||||
## 脚本配置:
|
## 服务器/云平台/Docker/Android 版
|
||||||
|
|
||||||
|
https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87
|
||||||
|
|
||||||
|
## App 版
|
||||||
|
|
||||||
### 1. Loon
|
### 1. Loon
|
||||||
安装使用 插件 [`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) 即可。
|
安装使用 插件 [`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
|
### 2. Surge
|
||||||
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)
|
#### 关于 Surge 的格外说明
|
||||||
|
|
||||||
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)
|
Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E)
|
||||||
|
|
||||||
|
定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)
|
||||||
|
|
||||||
|
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
|
||||||
|
|
||||||
|
1. 官方默认版模块(支持 App 内使用编辑参数): [`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)
|
||||||
|
|
||||||
|
> 最新版 Surge 已删除 `ability: http-client-policy` 参数, 模块暂不做修改, 对测落地功能无影响
|
||||||
|
|
||||||
|
2. 经典版, 不支持编辑参数, 固定带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者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
|
### 3. QX
|
||||||
订阅 重写 [`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) 即可。
|
订阅 重写 [`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) 即可。
|
||||||
|
|
||||||
|
定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
|
||||||
|
|
||||||
### 4. Stash
|
### 4. Stash
|
||||||
安装使用 覆写 [`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) 即可。
|
安装使用 覆写 [`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
|
### 5. Shadowrocket
|
||||||
安装使用 模块 [`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) 即可。
|
安装使用 模块 [`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) 即可。
|
||||||
|
|
||||||
|
### 6. Egern
|
||||||
|
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。
|
||||||
|
|
||||||
## 使用 Sub-Store
|
## 使用 Sub-Store
|
||||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||||
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
||||||
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
|
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
|
||||||
|
|
||||||
|
## 链接参数说明
|
||||||
|
|
||||||
|
https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
|
||||||
|
|
||||||
|
## 脚本使用说明
|
||||||
|
|
||||||
|
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
name: Sub-Store
|
name: Sub-Store
|
||||||
desc: 高级订阅管理工具 @Peng-YM
|
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
|
||||||
|
icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
|
||||||
|
|
||||||
http:
|
http:
|
||||||
mitm:
|
mitm:
|
||||||
@@ -19,18 +20,18 @@ http:
|
|||||||
cron:
|
cron:
|
||||||
script:
|
script:
|
||||||
- name: cron-sync-artifacts
|
- name: cron-sync-artifacts
|
||||||
cron: "0 0 * * *"
|
cron: "55 23 * * *"
|
||||||
timeout: 120
|
timeout: 120
|
||||||
|
|
||||||
script-providers:
|
script-providers:
|
||||||
sub-store-0:
|
sub-store-0:
|
||||||
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
|
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
|
||||||
interval: 86400
|
interval: 86400
|
||||||
|
|
||||||
sub-store-1:
|
sub-store-1:
|
||||||
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
|
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
|
||||||
interval: 86400
|
interval: 86400
|
||||||
|
|
||||||
cron-sync-artifacts:
|
cron-sync-artifacts:
|
||||||
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
|
||||||
interval: 86400
|
interval: 86400
|
||||||
|
|||||||
17
config/Surge-Beta.sgmodule
Normal file
17
config/Surge-Beta.sgmodule
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!name=Sub-Store(β)
|
||||||
|
#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
|
||||||
|
#!category=订阅管理
|
||||||
|
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,produce_sub:"sub1,sub2",produce_col:"col1,col2"
|
||||||
|
#!arguments-desc=\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅
|
||||||
|
|
||||||
|
[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://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
|
||||||
|
|
||||||
|
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
|
||||||
|
|
||||||
|
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}}
|
||||||
|
|
||||||
|
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
#!name=Sub-Store
|
#!name=Sub-Store
|
||||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
|
||||||
|
#!category=订阅管理
|
||||||
|
|
||||||
[MITM]
|
[MITM]
|
||||||
hostname = %APPEND% sub.store
|
hostname = %APPEND% sub.store
|
||||||
|
|
||||||
[Script]
|
[Script]
|
||||||
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 则可以使用此脚本
|
# 主程序 已经去掉 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 Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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 Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
|
||||||
|
|
||||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!name=Sub-Store
|
#!name=Sub-Store
|
||||||
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
|
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
|
||||||
|
#!category=订阅管理
|
||||||
|
|
||||||
[MITM]
|
[MITM]
|
||||||
hostname = %APPEND% sub.store
|
hostname = %APPEND% sub.store
|
||||||
|
|
||||||
[Script]
|
[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 Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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 Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
|
||||||
|
|
||||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
#!name=Sub-Store
|
#!name=Sub-Store
|
||||||
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
|
#!desc=支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
|
||||||
|
#!category=订阅管理
|
||||||
|
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync",timeout:120,engine:auto,produce:"# Sub-Store Produce",produce_cronexp:50 */6 * * *,produce_sub:"sub1,sub2",produce_col:"col1,col2"
|
||||||
|
#!arguments-desc=\n1️⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2️⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'\n\n3️⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4️⃣ timeout\n\n脚本超时, 单位为秒\n\n5️⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6️⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7️⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9️⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠️ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅
|
||||||
|
|
||||||
[MITM]
|
[MITM]
|
||||||
hostname = %APPEND% sub.store
|
hostname = %APPEND% sub.store
|
||||||
|
|
||||||
[Script]
|
[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
|
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
|
||||||
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
|
|
||||||
|
|
||||||
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
|
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
|
||||||
|
|
||||||
|
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}}
|
||||||
|
|
||||||
|
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"
|
||||||
292
scripts/demo.js
Normal file
292
scripts/demo.js
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
function operator(proxies = [], targetPlatform, context) {
|
||||||
|
// 支持快捷操作 不一定要写一个 function
|
||||||
|
// 可参考 https://t.me/zhetengsha/970
|
||||||
|
// https://t.me/zhetengsha/1009
|
||||||
|
|
||||||
|
// proxies 为传入的内部节点数组
|
||||||
|
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
|
||||||
|
// 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货
|
||||||
|
// 1. `_no-resolve` 为不解析域名
|
||||||
|
// 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
|
||||||
|
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
|
||||||
|
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
|
||||||
|
// 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名
|
||||||
|
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
|
||||||
|
// 7. `tls-fingerprint` 为 tls 指纹
|
||||||
|
// 8. `underlying-proxy` 为前置代理
|
||||||
|
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
|
||||||
|
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
||||||
|
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
||||||
|
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
|
||||||
|
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
|
||||||
|
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
|
||||||
|
// 15. `ip-version` 设置节点使用 IP 版本,可选:dual,ipv4,ipv6,ipv4-prefer,ipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
|
||||||
|
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
|
||||||
|
|
||||||
|
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
|
||||||
|
// 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件
|
||||||
|
// const fs = eval(`require("fs")`)
|
||||||
|
// // const path = eval(`require("path")`)
|
||||||
|
// fs.writeFileSync('/tmp/1.txt', $content, "utf8");
|
||||||
|
|
||||||
|
// $arguments 为传入的脚本参数
|
||||||
|
|
||||||
|
// $options 为通过链接传入的参数
|
||||||
|
// 例如: { arg1: 'a', arg2: 'b' }
|
||||||
|
// 可这样传:
|
||||||
|
// 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))
|
||||||
|
// /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D
|
||||||
|
// 或这样传:
|
||||||
|
// 先这样处理 encodeURIComponent('arg1=a&arg2=b')
|
||||||
|
// /api/file/foo?$options=arg1%3Da%26arg2%3Db
|
||||||
|
|
||||||
|
// console.log($options)
|
||||||
|
|
||||||
|
// targetPlatform 为输出的目标平台
|
||||||
|
|
||||||
|
// lodash
|
||||||
|
|
||||||
|
// $substore 为 OpenAPI
|
||||||
|
// 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md
|
||||||
|
|
||||||
|
// scriptResourceCache 缓存
|
||||||
|
// 可参考 https://t.me/zhetengsha/1003
|
||||||
|
// const cache = scriptResourceCache
|
||||||
|
// 设置
|
||||||
|
// cache.set('a:1', 1)
|
||||||
|
// cache.set('a:2', 2)
|
||||||
|
// 获取
|
||||||
|
// cache.get('a:1')
|
||||||
|
// 支持第二个参数: 自定义过期时间
|
||||||
|
// 支持第三个参数: 是否删除过期项
|
||||||
|
// cache.get('a:2', 1000, true)
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
// cache._cleanup()
|
||||||
|
// 支持第一个参数: 匹配前缀的项也一起删除
|
||||||
|
// 支持第二个参数: 自定义过期时间
|
||||||
|
// cache._cleanup('a:', 1000)
|
||||||
|
|
||||||
|
// 关于缓存时长
|
||||||
|
|
||||||
|
// 拉取 Sub-Store 订阅时, 会自动拉取远程订阅
|
||||||
|
|
||||||
|
// 远程订阅缓存是 1 小时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true
|
||||||
|
|
||||||
|
// 当使用相关脚本时, 若在对应的脚本中使用参数开启缓存, 可设置持久化缓存 sub-store-csr-expiration-time 的值来自定义默认缓存时长, 默认为 172800000 (48 * 3600 * 1000, 即 48 小时)
|
||||||
|
|
||||||
|
// 🎈Loon 可在插件中设置
|
||||||
|
|
||||||
|
// 其他平台同理, 持久化缓存数据在 JSON 里
|
||||||
|
|
||||||
|
// ProxyUtils 为节点处理工具
|
||||||
|
// 可参考 https://t.me/zhetengsha/1066
|
||||||
|
// const ProxyUtils = {
|
||||||
|
// parse, // 订阅解析
|
||||||
|
// process, // 节点操作/文件操作
|
||||||
|
// produce, // 输出订阅
|
||||||
|
// getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000)
|
||||||
|
// ipAddress, // https://github.com/beaugunderson/ip-address
|
||||||
|
// isIPv4,
|
||||||
|
// isIPv6,
|
||||||
|
// isIP,
|
||||||
|
// yaml, // yaml 解析和生成
|
||||||
|
// getFlag, // 获取 emoji 旗帜
|
||||||
|
// removeFlag, // 移除 emoji 旗帜
|
||||||
|
// getISO, // 获取 ISO 3166-1 alpha-2 代码
|
||||||
|
// Gist, // Gist 类
|
||||||
|
// download, // 内部的下载方法, 见 backend/src/utils/download.js
|
||||||
|
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
|
||||||
|
// isValidUUID, // 辅助判断是否为有效的 UUID
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
|
||||||
|
// ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一
|
||||||
|
// 示例: 给节点名添加前缀
|
||||||
|
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
|
||||||
|
// 示例: 给节点名添加旗帜
|
||||||
|
// $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`
|
||||||
|
|
||||||
|
// 示例: 从 sni 文件中读取内容并进行节点操作
|
||||||
|
// const sni = await produceArtifact({
|
||||||
|
// type: 'file',
|
||||||
|
// name: 'sni' // 文件名
|
||||||
|
// });
|
||||||
|
// $server.sni = sni
|
||||||
|
|
||||||
|
// 1. Surge 输出 WireGuard 完整配置
|
||||||
|
|
||||||
|
// let proxies = await produceArtifact({
|
||||||
|
// type: 'subscription',
|
||||||
|
// name: 'sub',
|
||||||
|
// platform: 'Surge',
|
||||||
|
// produceOpts: {
|
||||||
|
// 'include-unsupported-proxy': true,
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// $content = proxies
|
||||||
|
|
||||||
|
// 2. sing-box
|
||||||
|
|
||||||
|
// 但是一般不需要这样用, 可参考
|
||||||
|
// 1. https://t.me/zhetengsha/1111
|
||||||
|
// 2. https://t.me/zhetengsha/1070
|
||||||
|
// 3. https://t.me/zhetengsha/1241
|
||||||
|
|
||||||
|
// let singboxProxies = await produceArtifact({
|
||||||
|
// type: 'subscription', // type: 'subscription' 或 'collection'
|
||||||
|
// name: 'sub', // subscription name
|
||||||
|
// platform: 'sing-box', // target platform
|
||||||
|
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // JSON
|
||||||
|
// $content = JSON.stringify({}, null, 2)
|
||||||
|
|
||||||
|
// 3. clash.meta
|
||||||
|
|
||||||
|
// 但是一般不需要这样用, 可参考
|
||||||
|
// 1. https://t.me/zhetengsha/1111
|
||||||
|
// 2. https://t.me/zhetengsha/1070
|
||||||
|
// 3. https://t.me/zhetengsha/1234
|
||||||
|
|
||||||
|
// let clashMetaProxies = await produceArtifact({
|
||||||
|
// type: 'subscription',
|
||||||
|
// name: 'sub',
|
||||||
|
// platform: 'ClashMeta',
|
||||||
|
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
|
||||||
|
// 见 https://t.me/zhetengsha/1428
|
||||||
|
//
|
||||||
|
// const content = ProxyUtils.produce([...proxies], platform)
|
||||||
|
|
||||||
|
// // YAML
|
||||||
|
// ProxyUtils.yaml.load('YAML String')
|
||||||
|
// ProxyUtils.yaml.safeLoad('YAML String')
|
||||||
|
// $content = ProxyUtils.yaml.safeDump({})
|
||||||
|
// $content = ProxyUtils.yaml.dump({})
|
||||||
|
|
||||||
|
// 一个往文件里插入本地节点的例子:
|
||||||
|
// const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0])
|
||||||
|
// let clashMetaProxies = await produceArtifact({
|
||||||
|
// type: 'collection',
|
||||||
|
// name: '机场',
|
||||||
|
// platform: 'ClashMeta',
|
||||||
|
// produceType: 'internal'
|
||||||
|
// })
|
||||||
|
// yaml.proxies.unshift(...clashMetaProxies)
|
||||||
|
// $content = ProxyUtils.yaml.dump(yaml)
|
||||||
|
|
||||||
|
// { $content, $files, $options } will be passed to the next operator
|
||||||
|
// $content is the final content of the file
|
||||||
|
|
||||||
|
// flowUtils 为机场订阅流量信息处理工具
|
||||||
|
// 可参考:
|
||||||
|
// 1. https://t.me/zhetengsha/948
|
||||||
|
|
||||||
|
// context 为传入的上下文
|
||||||
|
// 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可)
|
||||||
|
|
||||||
|
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
|
||||||
|
|
||||||
|
// 若存在 `source._collection` 但 `source._collection.subscriptions` 中的 key 在 `source` 上不存在, 说明输出结果为组合订阅, 脚本设置在组合订阅上
|
||||||
|
|
||||||
|
// 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
|
||||||
|
|
||||||
|
// 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称
|
||||||
|
|
||||||
|
// let name = ''
|
||||||
|
// for (const [key, value] of Object.entries(env.source)) {
|
||||||
|
// if (!key.startsWith('_')) {
|
||||||
|
// name = value.displayName || value.name
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (!name) {
|
||||||
|
// const collection = env.source._collection
|
||||||
|
// name = collection.displayName || collection.name
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
|
||||||
|
// {
|
||||||
|
// "source": {
|
||||||
|
// "sub-1": {
|
||||||
|
// "name": "sub-1",
|
||||||
|
// "displayName": "",
|
||||||
|
// "mergeSources": "",
|
||||||
|
// "ignoreFailedRemoteSub": true,
|
||||||
|
// "process": [],
|
||||||
|
// "icon": "",
|
||||||
|
// "source": "local",
|
||||||
|
// "url": "",
|
||||||
|
// "content": "",
|
||||||
|
// "ua": "",
|
||||||
|
// "display-name": "",
|
||||||
|
// "useCacheForFailedRemoteSub": false
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "backend": "Node",
|
||||||
|
// "version": "2.14.198"
|
||||||
|
// }
|
||||||
|
// 2. 输出组合订阅 collection-1 时, 该组合订阅中的脚本上下文为:
|
||||||
|
// {
|
||||||
|
// "source": {
|
||||||
|
// "_collection": {
|
||||||
|
// "name": "collection-1",
|
||||||
|
// "displayName": "",
|
||||||
|
// "mergeSources": "",
|
||||||
|
// "ignoreFailedRemoteSub": false,
|
||||||
|
// "icon": "",
|
||||||
|
// "process": [],
|
||||||
|
// "subscriptions": [
|
||||||
|
// "sub-1"
|
||||||
|
// ],
|
||||||
|
// "display-name": ""
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "backend": "Node",
|
||||||
|
// "version": "2.14.198"
|
||||||
|
// }
|
||||||
|
// 3. 输出组合订阅 collection-1 时, 该组合订阅中的单条订阅 sub-1 中的某个脚本上下文为:
|
||||||
|
// {
|
||||||
|
// "source": {
|
||||||
|
// "sub-1": {
|
||||||
|
// "name": "sub-1",
|
||||||
|
// "displayName": "",
|
||||||
|
// "mergeSources": "",
|
||||||
|
// "ignoreFailedRemoteSub": true,
|
||||||
|
// "icon": "",
|
||||||
|
// "process": [],
|
||||||
|
// "source": "local",
|
||||||
|
// "url": "",
|
||||||
|
// "content": "",
|
||||||
|
// "ua": "",
|
||||||
|
// "display-name": "",
|
||||||
|
// "useCacheForFailedRemoteSub": false
|
||||||
|
// },
|
||||||
|
// "_collection": {
|
||||||
|
// "name": "collection-1",
|
||||||
|
// "displayName": "",
|
||||||
|
// "mergeSources": "",
|
||||||
|
// "ignoreFailedRemoteSub": false,
|
||||||
|
// "icon": "",
|
||||||
|
// "process": [],
|
||||||
|
// "subscriptions": [
|
||||||
|
// "sub-1"
|
||||||
|
// ],
|
||||||
|
// "display-name": ""
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "backend": "Node",
|
||||||
|
// "version": "2.14.198"
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 参数说明
|
||||||
|
// 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
|
||||||
|
|
||||||
|
console.log(JSON.stringify(context, null, 2));
|
||||||
|
|
||||||
|
return proxies;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* 【字体】
|
* 【字体】
|
||||||
* 可参考:https://www.dute.org/weird-fonts
|
* 可参考:https://www.dute.org/weird-fonts
|
||||||
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular
|
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)
|
||||||
*
|
*
|
||||||
* 【示例】
|
* 【示例】
|
||||||
* 1️⃣ 设置所有格式为 "serif-bold"
|
* 1️⃣ 设置所有格式为 "serif-bold"
|
||||||
@@ -31,6 +31,7 @@ function operator(proxies) {
|
|||||||
"double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","ℂ","𝔻","𝔼","𝔽","𝔾","ℍ","𝕀","𝕁","𝕂","𝕃","𝕄","ℕ","𝕆","ℙ","ℚ","ℝ","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐","ℤ"],
|
"double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","ℂ","𝔻","𝔼","𝔽","𝔾","ℍ","𝕀","𝕁","𝕂","𝕃","𝕄","ℕ","𝕆","ℙ","ℚ","ℝ","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐","ℤ"],
|
||||||
"circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
|
"circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
|
||||||
"square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
|
"square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
|
||||||
|
"modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"],
|
||||||
};
|
};
|
||||||
|
|
||||||
// charCode => index in `TABLE`
|
// charCode => index in `TABLE`
|
||||||
|
|||||||
79
scripts/ip-flag-node.js
Normal file
79
scripts/ip-flag-node.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const $ = $substore;
|
||||||
|
|
||||||
|
const {onlyFlagIP = true} = $arguments
|
||||||
|
|
||||||
|
async function operator(proxies) {
|
||||||
|
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 => {
|
||||||
|
if (onlyFlagIP && !ProxyUtils.isIP(proxy.server)) return;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return proxies;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function queryIpApi(proxy) {
|
||||||
|
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 result = new Promise((resolve, reject) => {
|
||||||
|
const url =
|
||||||
|
`http://ip-api.com/json/${encodeURIComponent(proxy.server)}?lang=zh-CN`;
|
||||||
|
$.http.get({
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
}).then(resp => {
|
||||||
|
const data = JSON.parse(resp.body);
|
||||||
|
if (data.status === "success") {
|
||||||
|
resolve(data.countryCode);
|
||||||
|
} else {
|
||||||
|
reject(new Error(data.message));
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
1
web
1
web
Submodule web deleted from b10b708c34
Reference in New Issue
Block a user