mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
1204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb5bc5765 | ||
|
|
0f0812ee6c | ||
|
|
6be158f17a | ||
|
|
c7c9b21f79 | ||
|
|
d84f761b5d | ||
|
|
b7d7346ef7 | ||
|
|
cbfe528c5e | ||
|
|
59ea2bd174 | ||
|
|
1c9ae2a079 | ||
|
|
22e39dc18f | ||
|
|
4276869033 | ||
|
|
c902ad8c87 | ||
|
|
10fe493162 | ||
|
|
3f9867512b | ||
|
|
8ab694efcc | ||
|
|
4977c4ac43 | ||
|
|
84144ad057 | ||
|
|
4c871964b0 | ||
|
|
b3d66d42ff | ||
|
|
65cdaa0946 | ||
|
|
a8135c7d6c | ||
|
|
d752690129 | ||
|
|
43f7ae4a9a | ||
|
|
9b3f76be19 | ||
|
|
7310d9bd66 | ||
|
|
d1551c5644 | ||
|
|
785a715ce2 | ||
|
|
86b828fd40 | ||
|
|
2a8db4a21c | ||
|
|
d0da52fff9 | ||
|
|
146c2d966f | ||
|
|
8059a23cf9 | ||
|
|
437c95925e | ||
|
|
6bbb3a1ccf | ||
|
|
dfaa02aa1a | ||
|
|
5b554b27e4 | ||
|
|
1d1ac03e44 | ||
|
|
9ce1ebb816 | ||
|
|
73fd36ceca | ||
|
|
4e1b776785 | ||
|
|
7e6476aecd | ||
|
|
e043a37b59 | ||
|
|
8b605e10a0 | ||
|
|
1945455ba6 | ||
|
|
427faed8d8 | ||
|
|
843cff42c4 | ||
|
|
80f44884fb | ||
|
|
6cfcd1c6e2 | ||
|
|
956fff1b98 | ||
|
|
bd2b3f3fdf | ||
|
|
2093baaedb | ||
|
|
1cd740aca0 | ||
|
|
0f1b61b5a7 | ||
|
|
88283211cb | ||
|
|
1874c20c6d | ||
|
|
4558ee8c91 | ||
|
|
cd2fd624db | ||
|
|
0c17e14d9f | ||
|
|
f0c0361174 | ||
|
|
fe8e64dfbf | ||
|
|
f962b41ccb | ||
|
|
75e4225d9c | ||
|
|
0b22492fbc | ||
|
|
7d35911f7f | ||
|
|
960706b1cc | ||
|
|
ebb5ef68cb | ||
|
|
7f67f57031 | ||
|
|
9dabe7826a | ||
|
|
3daa85683b | ||
|
|
1adb27bbcf | ||
|
|
93bbb3b345 | ||
|
|
0f10978b65 | ||
|
|
7029802b88 | ||
|
|
ecdbd01bc6 | ||
|
|
17a50cf0ec | ||
|
|
b70c689b61 | ||
|
|
dde77b5333 | ||
|
|
6285475873 | ||
|
|
f80a34d830 | ||
|
|
5473a9b627 | ||
|
|
4a19d5a0df | ||
|
|
8a4f510ca5 | ||
|
|
32d968bb8e | ||
|
|
edb1b9f6dc | ||
|
|
fa813a0040 | ||
|
|
db2be960ac | ||
|
|
e2ed426d96 | ||
|
|
6331cdd8cc | ||
|
|
36fc22d9da | ||
|
|
a9386242cd | ||
|
|
8a128ee31b | ||
|
|
c7f9326ef9 | ||
|
|
082b0a56ad | ||
|
|
e4e0c7e69c | ||
|
|
06acaa905a | ||
|
|
bd4ab1440a | ||
|
|
b4e24a9bc7 | ||
|
|
7c68c272ee | ||
|
|
a080d94f8d | ||
|
|
dd03f0a25f | ||
|
|
b677c01c3f | ||
|
|
96e6f63c88 | ||
|
|
2c479aceef | ||
|
|
6d488007f2 | ||
|
|
27fa8aca15 | ||
|
|
e87eaddb60 | ||
|
|
2805a0b477 | ||
|
|
d4378025f9 | ||
|
|
81a0cfdb4f | ||
|
|
32cdddd934 | ||
|
|
883e091930 | ||
|
|
9e1803e795 | ||
|
|
a75a845c24 | ||
|
|
ef7bfb11f7 | ||
|
|
63d5b3e6f7 | ||
|
|
5c98e6ac9f | ||
|
|
e320c258fa | ||
|
|
753523cdfe | ||
|
|
94c313e9b9 | ||
|
|
28b13dd7ac | ||
|
|
d7930bfc6b | ||
|
|
b95eb39694 | ||
|
|
5470bceee1 | ||
|
|
5c761a4137 | ||
|
|
1fe54a699a | ||
|
|
78037af70c | ||
|
|
54c9e66bab | ||
|
|
e7bd21cd34 | ||
|
|
422e161c5b | ||
|
|
2f7f9a93e2 | ||
|
|
bd0a2b3eeb | ||
|
|
515ba0183d | ||
|
|
2828122098 | ||
|
|
3ab0bfdca5 | ||
|
|
177ec57a81 | ||
|
|
32407e6071 | ||
|
|
fb4b606fb3 | ||
|
|
b27d348d94 | ||
|
|
49cd9762db | ||
|
|
109752b5c3 | ||
|
|
0092efd95c | ||
|
|
a53748ebda | ||
|
|
592dc36f81 | ||
|
|
cd51eaae0a | ||
|
|
bdca115229 | ||
|
|
ab632087aa | ||
|
|
a84c2cc6cd | ||
|
|
d28c8eb9c1 | ||
|
|
4cc562f48c | ||
|
|
a36de02496 | ||
|
|
47f09cb6b8 | ||
|
|
01ff77fa99 | ||
|
|
24c448b8ac | ||
|
|
e56bc1eed4 | ||
|
|
48550cb71c | ||
|
|
3d835c63d6 | ||
|
|
daaa4a70d4 | ||
|
|
6c2e5fe9ca | ||
|
|
879b728600 | ||
|
|
01c183c41a | ||
|
|
4234dc8b20 | ||
|
|
31053ca0d1 | ||
|
|
7233c7816f | ||
|
|
248fe8cdcc | ||
|
|
27ceb9f8ca | ||
|
|
7ec1a19ff8 | ||
|
|
2b4cefcc12 | ||
|
|
f92c4799d1 | ||
|
|
7459821d84 | ||
|
|
71a26c0750 | ||
|
|
012b580f5e | ||
|
|
08e6835c2d | ||
|
|
8ecfeed953 | ||
|
|
01f781a270 | ||
|
|
d3b282f864 | ||
|
|
28d930ad7e | ||
|
|
d4813ea124 | ||
|
|
a18ba58860 | ||
|
|
69d1f87249 | ||
|
|
d786601e30 | ||
|
|
857e736274 | ||
|
|
ea17ca6089 | ||
|
|
fe94b2e76d | ||
|
|
8228189476 | ||
|
|
121a2d3c52 | ||
|
|
5b46a25448 | ||
|
|
8bb19debdc | ||
|
|
0292124f4b | ||
|
|
f35ddb8160 | ||
|
|
597062b1ac | ||
|
|
7dcfb22911 | ||
|
|
c0e5ff2d48 | ||
|
|
9e47b1baf6 | ||
|
|
84875fdba5 | ||
|
|
eae9f0869c | ||
|
|
fcef8e755e | ||
|
|
fc66309a0f | ||
|
|
3a4de52c20 | ||
|
|
354c2c0b1b | ||
|
|
7403946d80 | ||
|
|
39f7355e02 | ||
|
|
f60731b36a | ||
|
|
6099c2bd97 | ||
|
|
bf2e80cf30 | ||
|
|
c4a682baba | ||
|
|
607420bb0d | ||
|
|
438a3a3db3 | ||
|
|
3eee8a5102 | ||
|
|
4ad7803511 | ||
|
|
b40d312141 | ||
|
|
b053482435 | ||
|
|
41e5964fa4 | ||
|
|
ab4d9b4c7e | ||
|
|
bf42d6ec02 | ||
|
|
856b6c1be0 | ||
|
|
46f8296667 | ||
|
|
75a53b23b8 | ||
|
|
1b08cb8ae1 | ||
|
|
9e6a147703 | ||
|
|
44ede4d7e7 | ||
|
|
bd44e81ed9 | ||
|
|
4d51172919 | ||
|
|
d51577aedb | ||
|
|
c1a7a313c7 | ||
|
|
e16f21b102 | ||
|
|
00bda4b257 | ||
|
|
4cf920438b | ||
|
|
e238a033dc | ||
|
|
221acdaf23 | ||
|
|
4331fd2138 | ||
|
|
c4f82642b9 | ||
|
|
9cf33887c6 | ||
|
|
40e651d301 | ||
|
|
1ff6de9979 | ||
|
|
5335fc17d8 | ||
|
|
451c6fa2ad | ||
|
|
11c9d20f65 | ||
|
|
816de94599 | ||
|
|
190f358b8f | ||
|
|
145fa83224 | ||
|
|
d659bdcfb5 | ||
|
|
7605ca39de | ||
|
|
f3862eb962 | ||
|
|
629f584e2f | ||
|
|
074026a997 | ||
|
|
6ba6365969 | ||
|
|
5dcd708f9c | ||
|
|
d15514f2df | ||
|
|
746c5975d7 | ||
|
|
a8391ad8e6 | ||
|
|
853c032872 | ||
|
|
5d60afb957 | ||
|
|
95a0f45c34 | ||
|
|
6e713f75f3 | ||
|
|
66bc15cea5 | ||
|
|
5026a74556 | ||
|
|
6d7255dc05 | ||
|
|
a10317844f | ||
|
|
a053f907fc | ||
|
|
58eaa91c66 | ||
|
|
d5d001c109 | ||
|
|
d92feb8c25 | ||
|
|
4dd42e8077 | ||
|
|
0d761c79ef | ||
|
|
3ff2ad5873 | ||
|
|
64d13d954d | ||
|
|
773cb16e91 | ||
|
|
3bf81767c0 | ||
|
|
57b9d97e1e | ||
|
|
facfdac06f | ||
|
|
e865459467 | ||
|
|
340e20b318 | ||
|
|
004e0a575d | ||
|
|
241be53cb5 | ||
|
|
314989aa55 | ||
|
|
a93f13d295 | ||
|
|
2bb19ae8be | ||
|
|
efd6e71c9b | ||
|
|
d52d0f50f3 | ||
|
|
d71d1296f8 | ||
|
|
deaa1630a1 | ||
|
|
63814b3c3c | ||
|
|
d66f4d5e54 | ||
|
|
21810be696 | ||
|
|
a1a522c472 | ||
|
|
62295895a9 | ||
|
|
ed5b395afc | ||
|
|
8219a09f53 | ||
|
|
b0117ca82d | ||
|
|
168c7f597a | ||
|
|
69a6480477 | ||
|
|
ac75b9e78b | ||
|
|
6d86235040 | ||
|
|
4b307067d0 | ||
|
|
d7e5a133c9 | ||
|
|
7c4b8fd8a5 | ||
|
|
b4ec6cfd0d | ||
|
|
94a609f08e | ||
|
|
ebcf1265e6 | ||
|
|
abf3c84cd4 | ||
|
|
c63d9a304e | ||
|
|
4a96716cd9 | ||
|
|
0b67abf4f6 | ||
|
|
2aa4308c83 | ||
|
|
82a762b0f6 | ||
|
|
21f6f7721c | ||
|
|
93f3ab7f44 | ||
|
|
57d7d98507 | ||
|
|
80880f066e | ||
|
|
8781f476cc | ||
|
|
7217055c15 | ||
|
|
18834895d8 | ||
|
|
7f6d132eb9 | ||
|
|
48e8c2a8af | ||
|
|
d5a0c8839a | ||
|
|
05f0dbedcf | ||
|
|
11db3cfdac | ||
|
|
5fce41347a | ||
|
|
169bd88bef | ||
|
|
d6e86a3176 | ||
|
|
5bcf5d557a | ||
|
|
4a2211becd | ||
|
|
45f00fc002 | ||
|
|
1da129a76c | ||
|
|
520e3f9c15 | ||
|
|
6f18dae272 | ||
|
|
b08f6ea13b | ||
|
|
38cc9a5a47 | ||
|
|
0f94d4e964 | ||
|
|
29c02277bf | ||
|
|
3717488095 | ||
|
|
9a2216fdd1 | ||
|
|
4a2815125b | ||
|
|
50e4dbc53e | ||
|
|
aa68018ad0 | ||
|
|
cc4d862335 | ||
|
|
adeff19c4d | ||
|
|
08aaad3bf5 | ||
|
|
0e8328dc10 | ||
|
|
bc2afca4ff | ||
|
|
9250e90848 | ||
|
|
c45310e731 | ||
|
|
b90c566380 | ||
|
|
26e9b1b6ef | ||
|
|
1d0ff6473a | ||
|
|
8c1d478941 | ||
|
|
f24bb15394 | ||
|
|
56d7c01b8b | ||
|
|
b2b187f8e8 | ||
|
|
76da8d1c5c | ||
|
|
d7f7069ee0 | ||
|
|
0aef932843 | ||
|
|
78d9ffa290 | ||
|
|
ba13620701 | ||
|
|
6538205956 | ||
|
|
4424886899 | ||
|
|
aefc05b0d8 | ||
|
|
fedf0e5587 | ||
|
|
96e2152cec | ||
|
|
2825f13186 | ||
|
|
7a8be56cd8 | ||
|
|
707463e09e | ||
|
|
befe20c773 | ||
|
|
4c65f261ba | ||
|
|
78ac733ed9 | ||
|
|
8d1150e48d | ||
|
|
17fd3c1dcc | ||
|
|
21f728f324 | ||
|
|
09cd3c9ff7 | ||
|
|
e718e4871e | ||
|
|
891e9253ea | ||
|
|
591e3881d5 | ||
|
|
648717c0e3 | ||
|
|
69fa1978be | ||
|
|
45a710490e | ||
|
|
ddd7753bf6 | ||
|
|
16f3ab7272 | ||
|
|
fff63928c9 | ||
|
|
79748c7042 | ||
|
|
b30e5b4f71 | ||
|
|
87959922be | ||
|
|
34122f3971 | ||
|
|
d64e547c68 | ||
|
|
aaa073745b | ||
|
|
e6e86423aa | ||
|
|
66d0a6fd4c | ||
|
|
9c8cdb2ab0 | ||
|
|
2a1c88c496 | ||
|
|
1092d49d90 | ||
|
|
cbda600c32 | ||
|
|
0f1a65cc42 | ||
|
|
749dd59b43 | ||
|
|
640c194220 | ||
|
|
df502e6206 | ||
|
|
5ac4aa8cb4 | ||
|
|
39ac327444 | ||
|
|
d43ffe29f8 | ||
|
|
2a69fc3acd | ||
|
|
22ad63f3f1 | ||
|
|
73ac2e85d6 | ||
|
|
af263bbd0a | ||
|
|
684cf2f52d | ||
|
|
83eb46f455 | ||
|
|
394bc2428a | ||
|
|
5cca33d49b | ||
|
|
5ec36f082c | ||
|
|
878354c44c | ||
|
|
e4ccb549c5 | ||
|
|
496aa42e86 | ||
|
|
83275bbc59 | ||
|
|
2e5f719762 | ||
|
|
ac1a46e853 | ||
|
|
225a3373f2 | ||
|
|
8accee4084 | ||
|
|
f732add651 | ||
|
|
cd48bce840 | ||
|
|
d5f3f7f10e | ||
|
|
053327bf93 | ||
|
|
9846c262c7 | ||
|
|
dd3cf82660 | ||
|
|
c977ebaa25 | ||
|
|
cb1a1216b5 | ||
|
|
1d5c21839c | ||
|
|
07f0ea81ff | ||
|
|
70f3d4c341 | ||
|
|
8ff99c314c | ||
|
|
563cae90dd | ||
|
|
6081c185b4 | ||
|
|
26325d89c9 | ||
|
|
cbb0424bc3 | ||
|
|
ec96b1c910 | ||
|
|
6a5b483159 | ||
|
|
7fcd65bc6b | ||
|
|
25ad2847c3 | ||
|
|
e12234e33c | ||
|
|
021425c3b9 | ||
|
|
ae1dcd4372 | ||
|
|
6e94693ed2 | ||
|
|
43933d5c03 | ||
|
|
ffe0de5add | ||
|
|
054ea43c28 | ||
|
|
50b19026c9 | ||
|
|
64548f6e83 | ||
|
|
51e1596ab0 | ||
|
|
1fa6af63c5 | ||
|
|
5b87a7bbe0 | ||
|
|
79308ac256 | ||
|
|
9003ab1f10 | ||
|
|
99b39a9e2b | ||
|
|
1c1f009214 | ||
|
|
65c0b83982 | ||
|
|
319ae7e837 | ||
|
|
92a6775cdf | ||
|
|
69c3182311 | ||
|
|
28dddc1d28 | ||
|
|
8e1583742d | ||
|
|
66d2f65261 | ||
|
|
bcb8bd2882 | ||
|
|
d357ca4990 | ||
|
|
94970b6922 | ||
|
|
33de07268c | ||
|
|
3b0dd362ce | ||
|
|
b57c1999e7 | ||
|
|
ddf8cf5539 | ||
|
|
1c91fa05ac | ||
|
|
0ccd0e5451 | ||
|
|
17028f615b | ||
|
|
3cd103eb4b | ||
|
|
2a8483f22f | ||
|
|
4636012f19 | ||
|
|
47a38805b2 | ||
|
|
26d0efa4a5 | ||
|
|
4abb9d7444 | ||
|
|
14bc4334b4 | ||
|
|
ebec33d149 | ||
|
|
b5256fc950 | ||
|
|
0fb2a78474 | ||
|
|
ef3cedebd7 | ||
|
|
06aa928d03 | ||
|
|
4ed88777f2 | ||
|
|
87595c31b1 | ||
|
|
44fa0f761a | ||
|
|
236a588fb4 | ||
|
|
6c4b2b5484 | ||
|
|
e3c1533d40 | ||
|
|
4a070a72b0 | ||
|
|
220ab78b5b | ||
|
|
d42722bd8e | ||
|
|
692232fd55 | ||
|
|
12c48cb613 | ||
|
|
ddc832842c | ||
|
|
73e95302ce | ||
|
|
6ecf84c6cc | ||
|
|
bdc127c5e7 | ||
|
|
c5721618df | ||
|
|
b3a2c52e7c | ||
|
|
8d2340b63f | ||
|
|
26a094e525 | ||
|
|
b877529657 | ||
|
|
33d39bdef3 | ||
|
|
9772be82e7 | ||
|
|
1ae00b2305 | ||
|
|
e3aade7167 | ||
|
|
5f5219fd31 | ||
|
|
409b54bc70 | ||
|
|
63cad8b378 | ||
|
|
28b53e1240 | ||
|
|
f8df06d4a5 | ||
|
|
ab6f6f612d | ||
|
|
f1739c3127 | ||
|
|
85bfc1a434 | ||
|
|
a9e9a4a933 | ||
|
|
e23bd3e8fe | ||
|
|
7ebbb43a75 | ||
|
|
1bb7da4ec6 | ||
|
|
68086275f7 | ||
|
|
52d10bf4e6 | ||
|
|
54bfe655a4 | ||
|
|
5b587e425c | ||
|
|
0274fa2300 | ||
|
|
d2ef060d4c | ||
|
|
465f3e5cdc | ||
|
|
3cebb6e3f3 | ||
|
|
216313f9c1 | ||
|
|
158a50435f | ||
|
|
4ce70db88a | ||
|
|
a175ab4802 | ||
|
|
155ceb27ab | ||
|
|
fe0d5bb0d6 | ||
|
|
7dbbbe9af2 | ||
|
|
190a8b6fc3 | ||
|
|
c39930707b | ||
|
|
7045cbe5c8 | ||
|
|
319525d4a7 | ||
|
|
8689b08136 | ||
|
|
b463299e76 | ||
|
|
aaa1832145 | ||
|
|
82dfb79c26 | ||
|
|
00b12d5775 | ||
|
|
a4f30cced6 | ||
|
|
486d6a4d3e | ||
|
|
9da9419155 | ||
|
|
b20effb4ec | ||
|
|
23a0857bcf | ||
|
|
f45faa7763 | ||
|
|
1fa518ba9b | ||
|
|
bd66596647 | ||
|
|
dc57e0886a | ||
|
|
544d8d907d | ||
|
|
544f8de9c6 | ||
|
|
0b18118e60 | ||
|
|
eee68fd024 | ||
|
|
13db14b703 | ||
|
|
2c54275ea3 | ||
|
|
4bc372c6fc | ||
|
|
ddc2564188 | ||
|
|
74834feb6a | ||
|
|
f0eca8f031 | ||
|
|
50fe25ac1c | ||
|
|
90923b2650 | ||
|
|
76bc7c9bb1 | ||
|
|
49e975ba5e | ||
|
|
afb2b19c66 | ||
|
|
b879d16442 | ||
|
|
f9eae3ec10 | ||
|
|
a2272de03f | ||
|
|
1436d4bd4e | ||
|
|
038efd6da6 | ||
|
|
c5dd77fab2 | ||
|
|
44556bc051 | ||
|
|
db67d0b809 | ||
|
|
f586ba09fd | ||
|
|
474d5fea57 | ||
|
|
a87eec14bd | ||
|
|
42ca08a970 | ||
|
|
cec5f34eb9 | ||
|
|
bfc00029ab | ||
|
|
f6c18367e9 | ||
|
|
ab6fc348b9 | ||
|
|
61df4d2144 | ||
|
|
628e280383 | ||
|
|
98a028e72a | ||
|
|
30ee7bb2a9 | ||
|
|
ed76f4df8f | ||
|
|
9344dc64b0 | ||
|
|
9c220490fb | ||
|
|
d83fec84b7 | ||
|
|
f192af0a5c | ||
|
|
a14c87095f | ||
|
|
b149a74785 | ||
|
|
6f70cb323d | ||
|
|
4b3fbb1400 | ||
|
|
5f72443f58 | ||
|
|
c02cc4bc62 | ||
|
|
d8ac3fc0dd | ||
|
|
00d0bd54fb | ||
|
|
3717630c49 | ||
|
|
10e1bfd1e4 | ||
|
|
b049f12e5b | ||
|
|
e21a25e8f4 | ||
|
|
5889bfa6cc | ||
|
|
337a0218fb | ||
|
|
2445a683a4 | ||
|
|
e5d8bfa29f | ||
|
|
c5c8ba9bbc | ||
|
|
7b9ff7dfe0 | ||
|
|
243e0ee457 | ||
|
|
6c12216424 | ||
|
|
a0cf875b4f | ||
|
|
da05d68cef | ||
|
|
6c9c9ba056 | ||
|
|
dcdeac0b13 | ||
|
|
a7e96cd696 | ||
|
|
9574ddc090 | ||
|
|
43ea98794e | ||
|
|
76c67c14b8 | ||
|
|
662268c546 | ||
|
|
6d53326919 | ||
|
|
66ff92fd79 | ||
|
|
610f71a33b | ||
|
|
9aee487fd6 | ||
|
|
4b4dbb5377 | ||
|
|
dd52573ada | ||
|
|
79108d18ac | ||
|
|
decce03905 | ||
|
|
92cb6446ad | ||
|
|
beef9ac1bb | ||
|
|
142ddbabb5 | ||
|
|
50af686f22 | ||
|
|
92d78b605a | ||
|
|
2015f60b1f | ||
|
|
22c7ab8d5d | ||
|
|
46498234b7 | ||
|
|
3a349e5f09 | ||
|
|
663bcc6d9e | ||
|
|
e5d8d49003 | ||
|
|
73b4f651d0 | ||
|
|
a664da6541 | ||
|
|
64dcb8dc53 | ||
|
|
a034f5c98c | ||
|
|
aa76e53a70 | ||
|
|
3314ad5f0e | ||
|
|
cca259b63c | ||
|
|
27e8967df8 | ||
|
|
410b02d4c7 | ||
|
|
4695a65d7f | ||
|
|
b1d8d502f8 | ||
|
|
a49aaa4fab | ||
|
|
4af31dd922 | ||
|
|
09d4902f41 | ||
|
|
956fa20af5 | ||
|
|
fd68656a76 | ||
|
|
50ed7d7241 | ||
|
|
b176a313e2 | ||
|
|
18903710ac | ||
|
|
63c6b627cc | ||
|
|
f796f445b1 | ||
|
|
af7724612b | ||
|
|
723d5e6e1b | ||
|
|
33b345ad22 | ||
|
|
feb1b45102 | ||
|
|
f7bfaea4f0 | ||
|
|
105b91a699 | ||
|
|
b6e3d81807 | ||
|
|
7a76874a6d | ||
|
|
228c8862e1 | ||
|
|
0509530caa | ||
|
|
8515b8ad15 | ||
|
|
ad3b12fa3a | ||
|
|
9dec5d9ccf | ||
|
|
cc8ba7782e | ||
|
|
cdbd31f265 | ||
|
|
3c9da46f13 | ||
|
|
cfd12e0da7 | ||
|
|
a778d2e222 | ||
|
|
ed1fa5d675 | ||
|
|
32a664676f | ||
|
|
cbba784d84 | ||
|
|
b0c4b03175 | ||
|
|
fda828ceae | ||
|
|
9efc2087a1 | ||
|
|
71f4695c3b | ||
|
|
d5a4b24209 | ||
|
|
3cbeba07b6 | ||
|
|
6b1fa38b4b | ||
|
|
a6e54c7560 | ||
|
|
9937b07557 | ||
|
|
b2878d8a2a | ||
|
|
362cbe9686 | ||
|
|
e8e903f630 | ||
|
|
b57b520aa0 | ||
|
|
a77d5dd5c9 | ||
|
|
954f08fca0 | ||
|
|
6ac729c7e6 | ||
|
|
d97766f1ba | ||
|
|
f9b1f82e31 | ||
|
|
41816cb0d8 | ||
|
|
4ca9ab33b1 | ||
|
|
272974ff39 | ||
|
|
bdb8a963a1 | ||
|
|
a0ab168a42 | ||
|
|
9c59a8fe1b | ||
|
|
ba49a50580 | ||
|
|
d1fcd2c048 | ||
|
|
92880fece6 | ||
|
|
de2d78e371 | ||
|
|
3bae9dc896 | ||
|
|
e573aa0da7 | ||
|
|
b8b47c74dc | ||
|
|
cc593bddda | ||
|
|
3958333aee | ||
|
|
d5993db6cb | ||
|
|
b46c83453f | ||
|
|
994863739e | ||
|
|
0323551382 | ||
|
|
ac239e8ba2 | ||
|
|
3a3988ee52 | ||
|
|
0d7ae57daa | ||
|
|
02ea0f360c | ||
|
|
fdb89a3a67 | ||
|
|
54515fcc58 | ||
|
|
ad559cafac | ||
|
|
6b56a34d1a | ||
|
|
de621d90a1 | ||
|
|
95b3b046f3 | ||
|
|
b2384a8736 | ||
|
|
245afe43f2 | ||
|
|
56c2cfb903 | ||
|
|
a77f738676 | ||
|
|
def5c3d533 | ||
|
|
dc16b980d6 | ||
|
|
eb386a180f | ||
|
|
a09d91e8cd | ||
|
|
4a9b72b058 | ||
|
|
b0114f2b29 | ||
|
|
4bb8ad9626 | ||
|
|
1502c40b3d | ||
|
|
0d84a7bd6b | ||
|
|
2e80c13fa5 | ||
|
|
6ec276472d | ||
|
|
79e748d98b | ||
|
|
2fe79008bc | ||
|
|
cad433b84c | ||
|
|
66f7ef047e | ||
|
|
6445374b2e | ||
|
|
17e47d40f0 | ||
|
|
823fe52933 | ||
|
|
f76a1aa267 | ||
|
|
a2b57f7885 | ||
|
|
5ca0dfdcca | ||
|
|
140450d3c7 | ||
|
|
ad94c25914 | ||
|
|
cfc6a944fa | ||
|
|
1449313265 | ||
|
|
0d58b4c845 | ||
|
|
c82c8db56b | ||
|
|
501e153dbf | ||
|
|
e2d0428d99 | ||
|
|
4c446e8335 | ||
|
|
ed359c28e3 | ||
|
|
c3a1d9cec6 | ||
|
|
a1b715db0c | ||
|
|
a07c055152 | ||
|
|
1843d3d083 | ||
|
|
c0d8f51523 | ||
|
|
dd88e6c693 | ||
|
|
68da7052fa | ||
|
|
cafce794e9 | ||
|
|
cb745297e5 | ||
|
|
3a27c9ac9c | ||
|
|
6af8c916b7 | ||
|
|
078e813a29 | ||
|
|
bef9652732 | ||
|
|
b207fb284f | ||
|
|
315cf22c54 | ||
|
|
8d5b2c7348 | ||
|
|
ec4e415df4 | ||
|
|
4d69dc2e78 | ||
|
|
2251565e24 | ||
|
|
e56c9c84dd | ||
|
|
e2c35d4e46 | ||
|
|
60c1c3d2a5 | ||
|
|
6e2d9024a6 | ||
|
|
baa130b0f7 | ||
|
|
7b2b246b92 | ||
|
|
b3c80147dc | ||
|
|
ac60d34866 | ||
|
|
e9b9c78101 | ||
|
|
164a9cb635 | ||
|
|
70fbb79d7b | ||
|
|
78479529ae | ||
|
|
694cf93f85 | ||
|
|
5059269227 | ||
|
|
8b8bfe123e | ||
|
|
1f05f42649 | ||
|
|
903d7d8bbe | ||
|
|
3541f24da7 | ||
|
|
93a98e4cec | ||
|
|
f27ce7ee48 | ||
|
|
8a95e17637 | ||
|
|
97b7b0c774 | ||
|
|
bd63842c50 | ||
|
|
392def5ac1 | ||
|
|
d7b5d42148 | ||
|
|
f62a79fedb | ||
|
|
1a85895c01 | ||
|
|
d234a8c314 | ||
|
|
5d57ce2859 | ||
|
|
bdb8096339 | ||
|
|
b767c8146b | ||
|
|
9b2adfacc6 | ||
|
|
e41e4968cd | ||
|
|
44622b43dc | ||
|
|
6ca5a525a0 | ||
|
|
5698208a47 | ||
|
|
51e3593ac2 | ||
|
|
0f7e995fc0 | ||
|
|
15511b0552 | ||
|
|
03a64823e4 | ||
|
|
51353fbd5d | ||
|
|
134375c31b | ||
|
|
22fc800e45 | ||
|
|
41478d28e8 | ||
|
|
72f7606062 | ||
|
|
ee13ebba0b | ||
|
|
449bfe6fca | ||
|
|
ca9ca76ad9 | ||
|
|
1e07a3c733 | ||
|
|
8679e6b240 | ||
|
|
8b3bbb5e04 | ||
|
|
2954b1af40 | ||
|
|
993658ff0b | ||
|
|
1f20c00c33 | ||
|
|
62e100ba55 | ||
|
|
8e5a85a402 | ||
|
|
6d5e8cd674 | ||
|
|
e4ad4df1e0 | ||
|
|
2bb27fbcb7 | ||
|
|
83d7d789a9 | ||
|
|
b50e0b3523 | ||
|
|
5a5b39a3ca | ||
|
|
1304c3f35b | ||
|
|
55982972b2 | ||
|
|
4ff1317074 | ||
|
|
b4d5003d0e | ||
|
|
818e94f41d | ||
|
|
8647f9fd59 | ||
|
|
c929bb3e48 | ||
|
|
002428d8ff | ||
|
|
f8671dc8a9 | ||
|
|
0e52e3c67c | ||
|
|
12acadb5c4 | ||
|
|
2419ae5374 | ||
|
|
0e196dbed7 | ||
|
|
c84aa4eb8b | ||
|
|
33c974ca93 | ||
|
|
107f04067f | ||
|
|
28fa2b8fb7 | ||
|
|
f885d171a5 | ||
|
|
c74834a9b1 | ||
|
|
161f455da1 | ||
|
|
6657fe1b05 | ||
|
|
0225e5f081 | ||
|
|
3cfabe703d | ||
|
|
b3630ae9e2 | ||
|
|
2359a221d7 | ||
|
|
e38f056a41 | ||
|
|
3999c61987 | ||
|
|
2c84b3d6cd | ||
|
|
def42d683c | ||
|
|
5cb2e3c105 | ||
|
|
092e873eb8 | ||
|
|
98af5051cb | ||
|
|
62501fd2dd | ||
|
|
7f6d55c635 | ||
|
|
8082c0d28d | ||
|
|
287082027e | ||
|
|
da44dc9cab | ||
|
|
fc86f3e15d | ||
|
|
56abcf29f7 | ||
|
|
e916c934e6 | ||
|
|
3db239115a | ||
|
|
f2538a2636 | ||
|
|
8451cfe0d6 | ||
|
|
30e5e079d5 | ||
|
|
b46fc08d76 | ||
|
|
4cb6b295f0 | ||
|
|
a95cb94ee7 | ||
|
|
40f6052231 | ||
|
|
a72a879a23 | ||
|
|
e4034ad480 | ||
|
|
e7f0259eaf | ||
|
|
469baf8281 | ||
|
|
4ab1e3cdaa | ||
|
|
dcc017182f | ||
|
|
020ba67bdb | ||
|
|
4a35f1293c | ||
|
|
b9f4c2e596 | ||
|
|
1ba6211c7c | ||
|
|
f0a286e516 | ||
|
|
f77aef65d8 | ||
|
|
8aa849312e | ||
|
|
7e1de6e49f | ||
|
|
f6237c8f58 | ||
|
|
63728b8423 | ||
|
|
9dea4800f7 | ||
|
|
9980101d2e | ||
|
|
6b9372ed37 | ||
|
|
6f4ca7f1b3 | ||
|
|
c78800b85d | ||
|
|
ecd33ef604 | ||
|
|
5f4622c039 | ||
|
|
74e9087460 | ||
|
|
24a27e51e3 | ||
|
|
d400be8b2e | ||
|
|
0185441ed2 | ||
|
|
f3b13a711e | ||
|
|
f56c4358f9 | ||
|
|
a9dc80cffa | ||
|
|
bbfd139ec8 | ||
|
|
7b4f75fddc | ||
|
|
8c7e8f01ee | ||
|
|
a3ddb13289 | ||
|
|
3f6b1356cb | ||
|
|
fb660ce957 | ||
|
|
b2987fa732 | ||
|
|
13dbe900fe | ||
|
|
3a1bc439d0 | ||
|
|
605d211dbd | ||
|
|
def4e496e4 | ||
|
|
a24525c30c | ||
|
|
9c30654d31 | ||
|
|
2da08c1817 | ||
|
|
693f23578e | ||
|
|
741121127b | ||
|
|
8a031ec767 | ||
|
|
f926e0fe92 | ||
|
|
b3b1cdea38 | ||
|
|
3ccd4cef52 | ||
|
|
5279f86ebf | ||
|
|
24e4f8d37a | ||
|
|
f84d3707bd | ||
|
|
8b4a440cdd | ||
|
|
2d4f589eb0 | ||
|
|
25cb0e7a69 | ||
|
|
242ec3673d | ||
|
|
caee36d818 | ||
|
|
c8efa40d81 | ||
|
|
64e57bd784 | ||
|
|
b6c5f5ae05 | ||
|
|
18851f182c | ||
|
|
8f7d58c5fb | ||
|
|
cbdf8f40e5 | ||
|
|
ff2efd2520 | ||
|
|
fd4ab77bf8 | ||
|
|
6ef0e591d7 | ||
|
|
cbd6311c2c | ||
|
|
6ad3ce4a18 | ||
|
|
2cff94e85d | ||
|
|
7e20815bd9 | ||
|
|
95ce460c7d | ||
|
|
0f99eb39cf | ||
|
|
77e66ffe2a | ||
|
|
869ca44da2 | ||
|
|
c4605eb127 | ||
|
|
9619fda7f1 | ||
|
|
4b0c1d9c70 | ||
|
|
c604cb6227 | ||
|
|
2f13938d1f | ||
|
|
0e953f0c47 | ||
|
|
16a1ec5641 | ||
|
|
0443a947d4 | ||
|
|
dd0a9ff0c2 | ||
|
|
2e75f172e4 | ||
|
|
05aa163cd5 | ||
|
|
9448cd2101 | ||
|
|
2a8f26c371 | ||
|
|
8324d59fa4 | ||
|
|
e89506817d | ||
|
|
f5cdb79446 | ||
|
|
5ca585659a | ||
|
|
0cd1d02b0a | ||
|
|
97291e4bbc | ||
|
|
c47770d58a | ||
|
|
ca6a16ccf4 | ||
|
|
31d8342f11 | ||
|
|
8986cefb4b | ||
|
|
ff21c4f554 | ||
|
|
24173fd7fe | ||
|
|
af7e719cce | ||
|
|
5edbffd1a6 | ||
|
|
bcfcd6d91d | ||
|
|
cba24939ea | ||
|
|
d39d3f6af0 | ||
|
|
d4c880e487 | ||
|
|
046b70a561 | ||
|
|
496af15335 | ||
|
|
e5cfa2c821 | ||
|
|
810f60f829 | ||
|
|
baf462c222 | ||
|
|
5d8a337cda | ||
|
|
b9376e8ad6 | ||
|
|
4c6a2a6118 | ||
|
|
8ea91879af | ||
|
|
6689f29281 | ||
|
|
b85a90562a | ||
|
|
f1431aa1e4 | ||
|
|
d4110df6f2 | ||
|
|
49ca638979 | ||
|
|
c31b2e703e | ||
|
|
74daeb3035 | ||
|
|
71d7f35b06 | ||
|
|
c534a64ebc | ||
|
|
8c811ed474 | ||
|
|
22767adb5a | ||
|
|
c8ef7533a8 | ||
|
|
26f27d7296 | ||
|
|
e9f29fec4a | ||
|
|
c5eb4878f2 | ||
|
|
9060347fdd | ||
|
|
4f3b4bfcee | ||
|
|
299b287c22 | ||
|
|
038141e5f2 | ||
|
|
10102540ac | ||
|
|
8da264608a | ||
|
|
37370cccda | ||
|
|
4752438966 | ||
|
|
40da24a354 | ||
|
|
3f3f9af66a | ||
|
|
b5d78225d2 | ||
|
|
199a81bfd8 | ||
|
|
24cc4cbe8b | ||
|
|
63f871ef0c | ||
|
|
943ce1b28f | ||
|
|
e8234a75e3 | ||
|
|
6f5837ce73 | ||
|
|
40744a5bf6 | ||
|
|
1ab4f9c058 | ||
|
|
7cede18e85 | ||
|
|
159047d589 | ||
|
|
2eb3541462 | ||
|
|
b6dec7d509 | ||
|
|
943e75839d | ||
|
|
a04bfcc550 | ||
|
|
d04a6edd7e | ||
|
|
101036df84 | ||
|
|
a1e62f29be | ||
|
|
37062a38dd | ||
|
|
5176151938 | ||
|
|
8f6abcbd91 | ||
|
|
aacd3d3892 | ||
|
|
4ce1a372af | ||
|
|
5aa9b8ceef | ||
|
|
9b4ae402bb | ||
|
|
6055c222ca | ||
|
|
ceae75379a | ||
|
|
d856a422e5 | ||
|
|
a094de32b5 | ||
|
|
a064e0327d | ||
|
|
3d130a7cbf | ||
|
|
d9a1830a30 | ||
|
|
23c3deb495 | ||
|
|
d4966a793b | ||
|
|
abb7d61c72 | ||
|
|
e3e5635514 | ||
|
|
84cd596a1c | ||
|
|
a066d95267 | ||
|
|
5baaf91ee7 | ||
|
|
43d41270d1 | ||
|
|
f334f430d1 | ||
|
|
2f7023e9e6 | ||
|
|
f2f72c696f | ||
|
|
ff619f6040 | ||
|
|
4f2b5540cf | ||
|
|
06353a08ff | ||
|
|
54359b38dd | ||
|
|
c1a8b4ad5b | ||
|
|
3b6a3e955c | ||
|
|
30a452528a | ||
|
|
4e6e863017 | ||
|
|
393eb77fa9 | ||
|
|
6caaba35e4 | ||
|
|
fdf2d929f8 | ||
|
|
e6cf086ef8 | ||
|
|
0ddc112453 | ||
|
|
fbc3c4434a | ||
|
|
e44cf7c7f2 | ||
|
|
2f02c545be | ||
|
|
6b9999da70 | ||
|
|
44b6505cfe | ||
|
|
d058762b45 | ||
|
|
e6288b1355 | ||
|
|
f257c47fc1 | ||
|
|
c184c0ef96 | ||
|
|
8cf67a1aec | ||
|
|
1e6f0d9694 | ||
|
|
755ac5565c | ||
|
|
1717fee73e | ||
|
|
6d76cda39b | ||
|
|
8bae8e0ac5 | ||
|
|
ca31a50c48 | ||
|
|
d81a0e2d3f | ||
|
|
32595ee27e | ||
|
|
17d514c6da | ||
|
|
3c1e813f55 | ||
|
|
bb13f44fbd | ||
|
|
cf8d4678ff | ||
|
|
ae6c720b59 | ||
|
|
bdf19c95f6 | ||
|
|
28187d73f6 | ||
|
|
512c3fffd5 | ||
|
|
3f0292e6ac | ||
|
|
ef3d2fc351 | ||
|
|
de6570cf91 | ||
|
|
e05bf0decd | ||
|
|
c39fe4aac9 | ||
|
|
b8960a99dd | ||
|
|
173e9c05fd | ||
|
|
8bc0c78cb8 | ||
|
|
fc6964359f | ||
|
|
433ddf5c81 | ||
|
|
5d101b0e8b | ||
|
|
6faa20eb3b | ||
|
|
82b3d71678 | ||
|
|
b2f2697413 | ||
|
|
f1c64efb26 | ||
|
|
0a3c1ec0a5 | ||
|
|
82b9efe1df | ||
|
|
ecaeabd4f4 | ||
|
|
69c9d31012 | ||
|
|
669cd60dc0 | ||
|
|
4e01d5030c | ||
|
|
04e7591d62 | ||
|
|
fc734ac268 | ||
|
|
9acb381b24 | ||
|
|
4af5c0c4ac | ||
|
|
67fb47411f | ||
|
|
6f8986fb48 | ||
|
|
62cdcc8fc4 | ||
|
|
b751f2e719 | ||
|
|
85b02d8eb0 | ||
|
|
9b39a9dfa4 | ||
|
|
d60b7ff87c | ||
|
|
3d101cf22a | ||
|
|
f6f4467c07 | ||
|
|
8e3afe82d3 | ||
|
|
37512eaa00 | ||
|
|
7b008ef684 | ||
|
|
e493f7874a | ||
|
|
001ce3f96f | ||
|
|
a91d7bbfbb | ||
|
|
20fac381af | ||
|
|
5a30fb9549 | ||
|
|
7565b02647 | ||
|
|
f793e21f17 | ||
|
|
1b8c8599fa | ||
|
|
73c163098e | ||
|
|
bcbb18a07f | ||
|
|
01dd6c852e | ||
|
|
87e5671a6f | ||
|
|
62bf30db08 | ||
|
|
c066091b2d | ||
|
|
a5a5415fc0 | ||
|
|
43e9ef4a6f | ||
|
|
acc637a3bf | ||
|
|
42f61069dd | ||
|
|
f8bd4f6713 | ||
|
|
d6c438514b | ||
|
|
86c3abd11d | ||
|
|
d0b3a26d2d | ||
|
|
ee55ae887f | ||
|
|
30bfd1d522 | ||
|
|
e1717459d4 | ||
|
|
d3a4692a6a | ||
|
|
0dfadd86c2 | ||
|
|
2a7466bba8 | ||
|
|
2869e2479f | ||
|
|
9bfb0e6430 | ||
|
|
3a5e043f80 | ||
|
|
e66ce0769b | ||
|
|
21e7523457 | ||
|
|
f62959c5d1 | ||
|
|
e6cb50ee47 | ||
|
|
942acfcf19 | ||
|
|
9edbcb35d7 | ||
|
|
50d92ad971 | ||
|
|
c2999826c7 | ||
|
|
6d40d01c5e | ||
|
|
9f6fe2336b | ||
|
|
c05373b1c9 | ||
|
|
c908e6b190 | ||
|
|
82fc7e4f0f | ||
|
|
43998f9274 | ||
|
|
faab7338d5 | ||
|
|
b0aca387bb | ||
|
|
3657eddf35 | ||
|
|
a378d3e73a | ||
|
|
512e49915c | ||
|
|
d71fb0a1cf | ||
|
|
7c4a261104 | ||
|
|
02e3246388 | ||
|
|
fe2f37984f | ||
|
|
d7ac5d4a1d | ||
|
|
ff12e4ef16 |
84
.github/workflows/main.yml
vendored
Normal file
84
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "backend/package.json"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "backend/package.json"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: "master"
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
cd backend && pnpm i --no-frozen-lockfile
|
||||
# - name: Test
|
||||
# run: |
|
||||
# cd backend
|
||||
# pnpm test
|
||||
# - name: Build
|
||||
# run: |
|
||||
# cd backend
|
||||
# pnpm run build
|
||||
- name: Bundle
|
||||
run: |
|
||||
cd backend
|
||||
pnpm bundle:esbuild
|
||||
- id: tag
|
||||
name: Generate release tag
|
||||
run: |
|
||||
cd backend
|
||||
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
|
||||
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
|
||||
- name: Prepare release
|
||||
run: |
|
||||
cd backend
|
||||
pnpm i -D conventional-changelog-cli
|
||||
pnpm run changelog
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ success() }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body_path: ./backend/CHANGELOG.md
|
||||
tag_name: ${{ steps.tag.outputs.release_tag }}
|
||||
# generate_release_notes: true
|
||||
files: |
|
||||
./backend/sub-store.min.js
|
||||
./backend/dist/sub-store-0.min.js
|
||||
./backend/dist/sub-store-1.min.js
|
||||
./backend/dist/sub-store-parser.loon.min.js
|
||||
./backend/dist/cron-sync-artifacts.min.js
|
||||
./backend/dist/sub-store.bundle.js
|
||||
- name: Git push assets to "release" branch
|
||||
run: |
|
||||
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
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
.DS_Store
|
||||
# json config
|
||||
sub-store.json
|
||||
sub-store_*.json
|
||||
root.json
|
||||
|
||||
# Logs
|
||||
@@ -86,7 +88,7 @@ out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
# dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
@@ -117,4 +119,20 @@ dist
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
.pnp.*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Dist files
|
||||
backend/dist/*
|
||||
!backend/dist/.gitkeep
|
||||
backend/sub-store.min.js
|
||||
|
||||
CHANGELOG.md
|
||||
|
||||
5
.idea/.gitignore
generated
vendored
5
.idea/.gitignore
generated
vendored
@@ -1,5 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
12
.idea/MagicStore.iml
generated
12
.idea/MagicStore.iml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/dictionaries/pengym.xml
generated
7
.idea/dictionaries/pengym.xml
generated
@@ -1,7 +0,0 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="pengym">
|
||||
<words>
|
||||
<w>obfs</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="JSCheckFunctionSignatures" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/MagicStore.iml" filepath="$PROJECT_DIR$/.idea/MagicStore.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
663
LICENSE
Normal file
663
LICENSE
Normal file
@@ -0,0 +1,663 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2015 Ayuntamiento de Madrid
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
123
README.md
123
README.md
@@ -1,29 +1,76 @@
|
||||
# Sub-Store
|
||||
> This project is still under active development. Current version: v0.1 (backend only).
|
||||
<div align="center">
|
||||
<br>
|
||||
<img width="200" src="https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png" alt="Sub-Store">
|
||||
<br>
|
||||
<br>
|
||||
<h2 align="center">Sub-Store<h2>
|
||||
</div>
|
||||
|
||||
<p align="center" color="#6a737d">
|
||||
Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.
|
||||
</p>
|
||||
|
||||
[](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)
|
||||
|
||||
Core functionalities:
|
||||
|
||||
Subscription manager for QX, Loon and Surge.
|
||||
Core functionality:
|
||||
1. Conversion among various formats.
|
||||
2. Subscription formatting.
|
||||
3. Collect multiple subscription 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.
|
||||
|
||||
## 1. Subscription Conversion
|
||||
|
||||
### Supported Input Formats
|
||||
- [x] SS URI
|
||||
- [x] SSR URI
|
||||
- [x] V2RayN URI
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
|
||||
- [x] Surge (SS, VMess, Trojan, HTTP)
|
||||
|
||||
> ⚠️ 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. However, we have already supported some very common non-standard URIs (such as VMess, VLESS).
|
||||
|
||||
- [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
|
||||
|
||||
example: `socks5+tls://user:pass@ip:port#name`
|
||||
|
||||
- [x] URI(AnyTLS, SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
|
||||
> Please note, HTTP(s) does not have a standard URI format, so it is not supported. Please use other formats.
|
||||
- [x] Clash Proxies YAML
|
||||
- [x] Clash Proxy JSON(single line)
|
||||
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
|
||||
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
|
||||
- [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, AnyTLS)
|
||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
|
||||
|
||||
Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):
|
||||
|
||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||
|
||||
### Supported Target Platforms
|
||||
- [x] QX
|
||||
- [x] Loon
|
||||
|
||||
- [x] Plain JSON
|
||||
- [x] Stash
|
||||
- [x] Clash.Meta(mihomo)
|
||||
- [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 URI
|
||||
|
||||
Deprecated:
|
||||
|
||||
- [x] Clash
|
||||
|
||||
## 2. Subscription Formatting
|
||||
|
||||
### Filtering
|
||||
- [x] **Keyword filter**
|
||||
- [x] **Discard keywords filter**
|
||||
|
||||
- [x] **Regex filter**
|
||||
- [x] **Discard regex filter**
|
||||
- [x] **Region filter**
|
||||
@@ -32,13 +79,53 @@ Core functionality:
|
||||
- [x] **Script filter**
|
||||
|
||||
### Proxy Operations
|
||||
|
||||
- [x] **Set property operator**: set some proxy properties such as `udp`,`tfo`, `skip-cert-verify` etc.
|
||||
- [x] **Flag operator**: add flags or remove flags for proxies.
|
||||
- [x] **Sort operator**: sort proxies by name.
|
||||
- [x] **Keyword sort operator**: sort proxies by keywords (fallback to normal sort).
|
||||
- [x] **Keyword rename operator**: replace by keywords in proxy names.
|
||||
- [x] **Keyword delete operator**: delete by keywords in proxy names.
|
||||
- [x] **Regex sort operator**: sort proxies by keywords (fallback to normal sort).
|
||||
- [x] **Regex rename operator**: replace by regex in proxy names.
|
||||
- [x] **Regex delete operator**: delete by regex in proxy names.
|
||||
- [x] **Script operator**: modify proxy by script.
|
||||
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
|
||||
|
||||
### Development
|
||||
|
||||
Install `pnpm`
|
||||
|
||||
Go to `backend` directories, install node dependencies:
|
||||
|
||||
```
|
||||
pnpm i
|
||||
```
|
||||
|
||||
```
|
||||
SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
pnpm bundle:esbuild
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
This project is under the GPL V3 LICENSE.
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#sub-store-org/sub-store&Date)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
|
||||
- Special thanks to @Orz-3 and @58xinian for their awesome icons.
|
||||
|
||||
## Sponsors
|
||||
|
||||
[](https://yxvm.com)
|
||||
|
||||
[NodeSupport](https://github.com/NodeSeekDev/NodeSupport) sponsored this project.
|
||||
|
||||
27
backend/.babelrc
Normal file
27
backend/.babelrc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env"
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"babel-plugin-relative-path-import",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"rootPathPrefix": "@",
|
||||
"rootPathSuffix": "src"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
15
backend/.eslintrc.json
Normal file
15
backend/.eslintrc.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"ignorePatterns": ["*.min.js", "src/vendor/*.js"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
6
backend/.prettierrc.json
Normal file
6
backend/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
15
backend/banner
Normal file
15
backend/banner
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
|
||||
* ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
|
||||
* ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
|
||||
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
||||
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||
* Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket!
|
||||
* @updated: <%= updated %>
|
||||
* @version: <%= pkg.version %>
|
||||
* @author: Peng-YM
|
||||
* @github: https://github.com/sub-store-org/Sub-Store
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
|
||||
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');
|
||||
});
|
||||
51
backend/bundle.js
Normal file
51
backend/bundle.js
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/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();
|
||||
|
||||
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: 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');
|
||||
});
|
||||
0
backend/dist/.gitkeep
vendored
Normal file
0
backend/dist/.gitkeep
vendored
Normal file
118
backend/gulpfile.babel.js
Normal file
118
backend/gulpfile.babel.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import fs from 'fs';
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import prettier from 'gulp-prettier';
|
||||
import header from 'gulp-header';
|
||||
import eslint from 'gulp-eslint-new';
|
||||
import newFile from 'gulp-file';
|
||||
import path from 'path';
|
||||
import tap from 'gulp-tap';
|
||||
|
||||
import pkg from './package.json';
|
||||
|
||||
export function peggy() {
|
||||
return gulp.src('src/**/*.peg').pipe(
|
||||
tap(function (file) {
|
||||
const filename = path.basename(file.path).split('.')[0] + '.js';
|
||||
const raw = fs.readFileSync(file.path, 'utf8');
|
||||
const contents = `import * as peggy from 'peggy';
|
||||
const grammars = String.raw\`\n${raw}\n\`;
|
||||
let parser;
|
||||
export default function getParser() {
|
||||
if (!parser) {
|
||||
parser = peggy.generate(grammars);
|
||||
}
|
||||
return parser;
|
||||
}\n`;
|
||||
return newFile(filename, contents).pipe(
|
||||
gulp.dest(path.dirname(file.path)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function lint() {
|
||||
return gulp
|
||||
.src('src/**/*.js')
|
||||
.pipe(eslint({ fix: true }))
|
||||
.pipe(eslint.fix())
|
||||
.pipe(eslint.format())
|
||||
.pipe(eslint.failAfterError());
|
||||
}
|
||||
|
||||
export function styles() {
|
||||
return gulp
|
||||
.src('src/**/*.js')
|
||||
.pipe(
|
||||
prettier({
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
tabWidth: 4,
|
||||
bracketSpacing: true,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest((file) => file.base));
|
||||
}
|
||||
|
||||
function scripts(src, dest) {
|
||||
return () => {
|
||||
return browserify(src)
|
||||
.transform('babelify', {
|
||||
presets: [['@babel/preset-env']],
|
||||
plugins: [
|
||||
[
|
||||
'babel-plugin-relative-path-import',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
rootPathPrefix: '@',
|
||||
rootPathSuffix: 'src',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
})
|
||||
.plugin('tinyify')
|
||||
.bundle()
|
||||
.pipe(fs.createWriteStream(dest));
|
||||
};
|
||||
}
|
||||
|
||||
function banner(dest) {
|
||||
return () =>
|
||||
gulp
|
||||
.src(dest)
|
||||
.pipe(
|
||||
header(fs.readFileSync('./banner', 'utf-8'), {
|
||||
pkg,
|
||||
updated: new Date().toLocaleString('zh-CN'),
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest((file) => file.base));
|
||||
}
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
export const build = gulp.series(
|
||||
gulp.parallel(
|
||||
artifacts.map((artifact) => scripts(artifact.src, artifact.dest)),
|
||||
),
|
||||
gulp.parallel(artifacts.map((artifact) => banner(artifact.dest))),
|
||||
);
|
||||
|
||||
const all = gulp.series(peggy, lint, styles, build);
|
||||
|
||||
export default all;
|
||||
8
backend/jsconfig.json
Normal file
8
backend/jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,77 @@
|
||||
{
|
||||
"name": "sub-store-backend",
|
||||
"version": "0.0.1",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, and Surge.",
|
||||
"main": "sub-store.js",
|
||||
"name": "sub-store",
|
||||
"version": "2.20.2",
|
||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
|
||||
"serve": "node sub-store.min.js",
|
||||
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
|
||||
"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",
|
||||
"bundle": "node bundle.js",
|
||||
"bundle:esbuild": "node bundle-esbuild.js",
|
||||
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL"
|
||||
}
|
||||
"author": "Peng-YM",
|
||||
"license": "GPL-3.0",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@maxmind/geoip2-node": "^5.0.0",
|
||||
"automerge": "1.0.1-preview.7",
|
||||
"body-parser": "^1.19.0",
|
||||
"buffer": "^6.0.3",
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"cron": "^3.1.6",
|
||||
"dns-packet": "^5.6.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.17.1",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"ip-address": "^9.0.5",
|
||||
"js-base64": "^3.7.2",
|
||||
"json5": "^2.2.3",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"ms": "^2.1.3",
|
||||
"nanoid": "^3.3.3",
|
||||
"semver": "^7.6.3",
|
||||
"static-js-yaml": "^1.0.0",
|
||||
"undici": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.0",
|
||||
"@babel/node": "^7.17.10",
|
||||
"@babel/preset-env": "^7.18.0",
|
||||
"@babel/register": "^7.17.7",
|
||||
"@types/gulp": "^4.0.9",
|
||||
"axios": "^0.21.2",
|
||||
"babel-plugin-relative-path-import": "^2.0.1",
|
||||
"babelify": "^10.0.0",
|
||||
"browser-pack-flat": "^3.4.2",
|
||||
"browserify": "^17.0.0",
|
||||
"chai": "^4.3.6",
|
||||
"esbuild": "^0.19.8",
|
||||
"eslint": "^8.16.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-eslint-new": "^1.4.4",
|
||||
"gulp-file": "^0.4.0",
|
||||
"gulp-header": "^2.0.9",
|
||||
"gulp-prettier": "^4.0.0",
|
||||
"gulp-tap": "^2.0.0",
|
||||
"mocha": "^10.0.0",
|
||||
"nodemon": "^2.0.16",
|
||||
"peggy": "^2.0.1",
|
||||
"prettier": "2.6.2",
|
||||
"prettier-plugin-sort-imports": "^1.6.1",
|
||||
"tinyify": "^3.0.0"
|
||||
}
|
||||
}
|
||||
46
backend/patches/http-proxy@1.18.1.patch
Normal file
46
backend/patches/http-proxy@1.18.1.patch
Normal file
@@ -0,0 +1,46 @@
|
||||
diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js
|
||||
index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644
|
||||
--- a/lib/http-proxy/common.js
|
||||
+++ b/lib/http-proxy/common.js
|
||||
@@ -1,6 +1,5 @@
|
||||
var common = exports,
|
||||
url = require('url'),
|
||||
- extend = require('util')._extend,
|
||||
required = require('requires-port');
|
||||
|
||||
var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i,
|
||||
@@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) {
|
||||
);
|
||||
|
||||
outgoing.method = options.method || req.method;
|
||||
- outgoing.headers = extend({}, req.headers);
|
||||
+ outgoing.headers = Object.assign({}, req.headers);
|
||||
|
||||
if (options.headers){
|
||||
- extend(outgoing.headers, options.headers);
|
||||
+ Object.assign(outgoing.headers, options.headers);
|
||||
}
|
||||
|
||||
if (options.auth) {
|
||||
diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js
|
||||
index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644
|
||||
--- a/lib/http-proxy/index.js
|
||||
+++ b/lib/http-proxy/index.js
|
||||
@@ -1,5 +1,4 @@
|
||||
var httpProxy = module.exports,
|
||||
- extend = require('util')._extend,
|
||||
parse_url = require('url').parse,
|
||||
EE3 = require('eventemitter3'),
|
||||
http = require('http'),
|
||||
@@ -47,9 +46,9 @@ function createRightProxy(type) {
|
||||
args[cntr] !== res
|
||||
) {
|
||||
//Copy global options
|
||||
- requestOptions = extend({}, options);
|
||||
+ requestOptions = Object.assign({}, options);
|
||||
//Overwrite with request options
|
||||
- extend(requestOptions, args[cntr]);
|
||||
+ Object.assign(requestOptions, args[cntr]);
|
||||
|
||||
cntr--;
|
||||
}
|
||||
9247
backend/pnpm-lock.yaml
generated
Normal file
9247
backend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
backend/src/constants.js
Normal file
18
backend/src/constants.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export const SCHEMA_VERSION_KEY = 'schemaVersion';
|
||||
export const SETTINGS_KEY = 'settings';
|
||||
export const SUBS_KEY = 'subs';
|
||||
export const COLLECTIONS_KEY = 'collections';
|
||||
export const FILES_KEY = 'files';
|
||||
export const MODULES_KEY = 'modules';
|
||||
export const ARTIFACTS_KEY = 'artifacts';
|
||||
export const RULES_KEY = 'rules';
|
||||
export const TOKENS_KEY = 'tokens';
|
||||
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
||||
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
||||
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
||||
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
|
||||
export const 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 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
|
||||
4
backend/src/core/app.js
Normal file
4
backend/src/core/app.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { OpenAPI } from '@/vendor/open-api';
|
||||
|
||||
const $ = new OpenAPI('sub-store');
|
||||
export default $;
|
||||
653
backend/src/core/proxy-utils/index.js
Normal file
653
backend/src/core/proxy-utils/index.js
Normal file
@@ -0,0 +1,653 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import { Buffer } from 'buffer';
|
||||
import rs from '@/utils/rs';
|
||||
import YAML from '@/utils/yaml';
|
||||
import download, { downloadFile } from '@/utils/download';
|
||||
import {
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isValidPortNumber,
|
||||
isValidUUID,
|
||||
isNotBlank,
|
||||
ipAddress,
|
||||
getRandomPort,
|
||||
numberToString,
|
||||
} from '@/utils';
|
||||
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
|
||||
import PROXY_PREPROCESSORS from './preprocessors';
|
||||
import PROXY_PRODUCERS from './producers';
|
||||
import PROXY_PARSERS from './parsers';
|
||||
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';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
function preprocess(raw) {
|
||||
for (const processor of PROXY_PREPROCESSORS) {
|
||||
try {
|
||||
if (processor.test(raw)) {
|
||||
$.info(`Pre-processor [${processor.name}] activated`);
|
||||
return processor.parse(raw);
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parse(raw) {
|
||||
raw = preprocess(raw);
|
||||
// parse
|
||||
const lines = raw.split('\n');
|
||||
const proxies = [];
|
||||
let lastParser;
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (line.length === 0) continue; // skip empty line
|
||||
let success = false;
|
||||
|
||||
// try to parse with last used parser
|
||||
if (lastParser) {
|
||||
const [proxy, error] = tryParse(lastParser, line);
|
||||
if (!error) {
|
||||
proxies.push(lastParse(proxy));
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
// search for a new parser
|
||||
for (const parser of PROXY_PARSERS) {
|
||||
const [proxy, error] = tryParse(parser, line);
|
||||
if (!error) {
|
||||
proxies.push(lastParse(proxy));
|
||||
lastParser = parser;
|
||||
success = true;
|
||||
$.info(`${parser.name} is activated`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
$.error(`Failed to parse line: ${line}`);
|
||||
}
|
||||
}
|
||||
return proxies.filter((proxy) => {
|
||||
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 processFn(
|
||||
proxies,
|
||||
operators = [],
|
||||
targetPlatform,
|
||||
source,
|
||||
$options,
|
||||
) {
|
||||
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
|
||||
let script;
|
||||
let $arguments = {};
|
||||
if (item.type.indexOf('Script') !== -1) {
|
||||
const { mode, content } = item.args;
|
||||
if (mode === 'link') {
|
||||
let url = content || '';
|
||||
// extract link arguments
|
||||
const rawArgs = url.split('#');
|
||||
if (rawArgs.length > 1) {
|
||||
try {
|
||||
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
||||
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
||||
} catch (e) {
|
||||
for (const pair of rawArgs[1].split('&')) {
|
||||
const key = pair.split('=')[0];
|
||||
const value = pair.split('=')[1];
|
||||
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
||||
$arguments[key] =
|
||||
value == null || value === ''
|
||||
? true
|
||||
: decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
url = `${url.split('#')[0]}${
|
||||
rawArgs[2]
|
||||
? `#${rawArgs[2]}`
|
||||
: $arguments?.noCache != null ||
|
||||
$arguments?.insecure != null
|
||||
? `#${rawArgs[1]}`
|
||||
: ''
|
||||
}`;
|
||||
const downloadUrlMatch = url
|
||||
.split('#')[0]
|
||||
.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 (type === 'module') {
|
||||
script = item.content;
|
||||
} else {
|
||||
script = await produceArtifact({
|
||||
type: 'file',
|
||||
name,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法加载 ${type}: ${url}`);
|
||||
}
|
||||
} else if (url?.startsWith('/')) {
|
||||
try {
|
||||
const fs = eval(`require("fs")`);
|
||||
script = fs.readFileSync(url.split('#')[0], 'utf8');
|
||||
// $.info(`Script loaded: >>>\n ${script}`);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when reading local script: ${item.args.content}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法从该路径读取脚本文件: ${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 {
|
||||
script = content;
|
||||
$arguments = item.args.arguments || {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!PROXY_PROCESSORS[item.type]) {
|
||||
$.error(`Unknown operator: "${item.type}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
$.log(
|
||||
`Applying "${item.type}" with arguments:\n >>> ${
|
||||
JSON.stringify(item.args, null, 2) || 'None'
|
||||
}`,
|
||||
);
|
||||
let processor;
|
||||
if (item.type.indexOf('Script') !== -1) {
|
||||
processor = PROXY_PROCESSORS[item.type](
|
||||
script,
|
||||
targetPlatform,
|
||||
$arguments,
|
||||
source,
|
||||
$options,
|
||||
);
|
||||
} else {
|
||||
processor = PROXY_PROCESSORS[item.type](item.args || {});
|
||||
}
|
||||
proxies = await ApplyProcessor(processor, proxies);
|
||||
}
|
||||
return proxies;
|
||||
}
|
||||
|
||||
function produce(proxies, targetPlatform, type, opts = {}) {
|
||||
const producer = PROXY_PRODUCERS[targetPlatform];
|
||||
if (!producer) {
|
||||
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
|
||||
}
|
||||
|
||||
const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
|
||||
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') {
|
||||
let list = proxies
|
||||
.map((proxy) => {
|
||||
try {
|
||||
return producer.produce(proxy, type, opts);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Cannot produce proxy: ${JSON.stringify(
|
||||
proxy,
|
||||
null,
|
||||
2,
|
||||
)}\nReason: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.filter((line) => line.length > 0);
|
||||
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') {
|
||||
return producer.produce(proxies, type, opts);
|
||||
}
|
||||
}
|
||||
|
||||
export const ProxyUtils = {
|
||||
parse,
|
||||
process: processFn,
|
||||
produce,
|
||||
ipAddress,
|
||||
getRandomPort,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isIP,
|
||||
yaml: YAML,
|
||||
getFlag,
|
||||
removeFlag,
|
||||
getISO,
|
||||
MMDB,
|
||||
Gist,
|
||||
download,
|
||||
downloadFile,
|
||||
isValidUUID,
|
||||
doh,
|
||||
Buffer,
|
||||
Base64,
|
||||
JSON5,
|
||||
};
|
||||
|
||||
function tryParse(parser, line) {
|
||||
if (!safeMatch(parser, line)) return [null, new Error('Parser mismatch')];
|
||||
try {
|
||||
const proxy = parser.parse(line);
|
||||
return [proxy, null];
|
||||
} catch (err) {
|
||||
return [null, err];
|
||||
}
|
||||
}
|
||||
|
||||
function safeMatch(parser, line) {
|
||||
try {
|
||||
return parser.test(line);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
proxy.server = `${proxy.server}`
|
||||
.trim()
|
||||
.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.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
}
|
||||
if (['vless'].includes(proxy.type)) {
|
||||
if (!proxy.network) {
|
||||
proxy.network = 'tcp';
|
||||
}
|
||||
}
|
||||
if (
|
||||
[
|
||||
'trojan',
|
||||
'tuic',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'juicity',
|
||||
'anytls',
|
||||
].includes(proxy.type)
|
||||
) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (proxy.network) {
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
|
||||
if (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;
|
||||
delete proxy[`${proxy.network}-opts`].headers.host;
|
||||
}
|
||||
}
|
||||
if (proxy.network === 'h2') {
|
||||
const host = proxy['h2-opts']?.headers?.host;
|
||||
const path = proxy['h2-opts']?.path;
|
||||
if (host && !Array.isArray(host)) {
|
||||
proxy['h2-opts'].headers.host = [host];
|
||||
}
|
||||
if (Array.isArray(path)) {
|
||||
proxy['h2-opts'].path = path[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
|
||||
if (
|
||||
!proxy.tls &&
|
||||
['ws', 'http'].includes(proxy.network) &&
|
||||
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
|
||||
!isIP(proxy.server)
|
||||
) {
|
||||
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
|
||||
proxy[`${proxy.network}-opts`].headers =
|
||||
proxy[`${proxy.network}-opts`].headers || {};
|
||||
proxy[`${proxy.network}-opts`].headers.Host =
|
||||
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
|
||||
? [proxy.server]
|
||||
: proxy.server;
|
||||
}
|
||||
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
|
||||
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
|
||||
let transportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (transportHost && !Array.isArray(transportHost)) {
|
||||
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
|
||||
}
|
||||
if (transportPath && !Array.isArray(transportPath)) {
|
||||
proxy[`${proxy.network}-opts`].path = [transportPath];
|
||||
}
|
||||
}
|
||||
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 (
|
||||
['hysteria2'].includes(proxy.type) &&
|
||||
!proxy['obfs-password'] &&
|
||||
proxy['obfs_password']
|
||||
) {
|
||||
proxy['obfs-password'] = proxy['obfs_password'];
|
||||
delete proxy['obfs_password'];
|
||||
}
|
||||
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 (
|
||||
['ss'].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;
|
||||
}
|
||||
|
||||
function isIP(ip) {
|
||||
return isIPv4(ip) || isIPv6(ip);
|
||||
}
|
||||
1694
backend/src/core/proxy-utils/parsers/index.js
Normal file
1694
backend/src/core/proxy-utils/parsers/index.js
Normal file
File diff suppressed because it is too large
Load Diff
211
backend/src/core/proxy-utils/parsers/peggy/loon.js
Normal file
211
backend/src/core/proxy-utils/parsers/peggy/loon.js
Normal file
@@ -0,0 +1,211 @@
|
||||
import * as peggy from 'peggy';
|
||||
const grammars = String.raw`
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
}}
|
||||
|
||||
// per-parser initializer
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const transport = {};
|
||||
const $ = {};
|
||||
|
||||
function handleTransport() {
|
||||
if (transport.type === "tcp") { /* do nothing */ }
|
||||
else if (transport.type === "ws") {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", transport.path);
|
||||
$set(proxy, "ws-opts.headers.Host", transport.host);
|
||||
} else if (transport.type === "http") {
|
||||
proxy.network = "http";
|
||||
$set(proxy, "http-opts.path", transport.path);
|
||||
$set(proxy, "http-opts.headers.Host", transport.host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
|
||||
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/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{
|
||||
proxy.type = "ssr";
|
||||
// handle ssr obfs
|
||||
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/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle ss obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
proxy.plugin = "obfs";
|
||||
$set(proxy, "plugin-opts.mode", obfs.type);
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
handleTransport();
|
||||
}
|
||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
|
||||
proxy.type = "vless";
|
||||
handleTransport();
|
||||
}
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
address = comma server:server comma port:port {
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
}
|
||||
|
||||
server = ip/domain
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = j;
|
||||
$.ip = input.substring(start, j).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
throw new Error("Invalid domain: " + domain);
|
||||
}
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
}
|
||||
throw new Error("Invalid port number: " + port);
|
||||
}
|
||||
|
||||
method = comma cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
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 = & {
|
||||
let j = peg$currPos;
|
||||
let start, end;
|
||||
let first = true;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ',') {
|
||||
if (first) {
|
||||
start = j + 1;
|
||||
first = false;
|
||||
} else {
|
||||
end = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
j++;
|
||||
}
|
||||
const match = input.substring(start, end);
|
||||
if (match.indexOf("=") === -1) {
|
||||
$.username = match;
|
||||
peg$currPos = end;
|
||||
return true;
|
||||
}
|
||||
} { proxy.username = $.username; }
|
||||
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
|
||||
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
|
||||
|
||||
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
|
||||
|
||||
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
|
||||
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
|
||||
obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; }
|
||||
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
||||
obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
|
||||
uri = $[^,]+
|
||||
|
||||
transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; }
|
||||
transport_host = comma "host" equals host:domain { transport.host = host; }
|
||||
transport_path = comma "path" equals path:uri { transport.path = path; }
|
||||
|
||||
ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; }
|
||||
ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
|
||||
|
||||
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; }
|
||||
tls_name = comma sni:("tls-name") equals host:domain { proxy.sni = host; }
|
||||
sni = comma sni:("sni") equals host:domain { proxy.sni = host; }
|
||||
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'); }
|
||||
|
||||
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = 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; }
|
||||
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'; }
|
||||
|
||||
block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
`;
|
||||
let parser;
|
||||
export default function getParser() {
|
||||
if (!parser) {
|
||||
parser = peggy.generate(grammars);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
201
backend/src/core/proxy-utils/parsers/peggy/loon.peg
Normal file
201
backend/src/core/proxy-utils/parsers/peggy/loon.peg
Normal file
@@ -0,0 +1,201 @@
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
}}
|
||||
|
||||
// per-parser initializer
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const transport = {};
|
||||
const $ = {};
|
||||
|
||||
function handleTransport() {
|
||||
if (transport.type === "tcp") { /* do nothing */ }
|
||||
else if (transport.type === "ws") {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", transport.path);
|
||||
$set(proxy, "ws-opts.headers.Host", transport.host);
|
||||
} else if (transport.type === "http") {
|
||||
proxy.network = "http";
|
||||
$set(proxy, "http-opts.path", transport.path);
|
||||
$set(proxy, "http-opts.headers.Host", transport.host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
|
||||
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/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{
|
||||
proxy.type = "ssr";
|
||||
// handle ssr obfs
|
||||
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/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)* {
|
||||
proxy.type = "ss";
|
||||
// handle ss obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
proxy.plugin = "obfs";
|
||||
$set(proxy, "plugin-opts.mode", obfs.type);
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
|
||||
proxy.type = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
proxy.alterId = proxy.alterId || 0;
|
||||
handleTransport();
|
||||
}
|
||||
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
|
||||
proxy.type = "vless";
|
||||
handleTransport();
|
||||
}
|
||||
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleTransport();
|
||||
}
|
||||
hysteria2 = tag equals "hysteria2"i address password (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {
|
||||
proxy.type = "hysteria2";
|
||||
}
|
||||
https = tag equals "https"i address (username password)? (tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
proxy.tls = true;
|
||||
}
|
||||
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "http";
|
||||
}
|
||||
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_name/sni/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
address = comma server:server comma port:port {
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
}
|
||||
|
||||
server = ip/domain
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = j;
|
||||
$.ip = input.substring(start, j).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
throw new Error("Invalid domain: " + domain);
|
||||
}
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
}
|
||||
throw new Error("Invalid port number: " + port);
|
||||
}
|
||||
|
||||
method = comma cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
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 = & {
|
||||
let j = peg$currPos;
|
||||
let start, end;
|
||||
let first = true;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ',') {
|
||||
if (first) {
|
||||
start = j + 1;
|
||||
first = false;
|
||||
} else {
|
||||
end = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
j++;
|
||||
}
|
||||
const match = input.substring(start, end);
|
||||
if (match.indexOf("=") === -1) {
|
||||
$.username = match;
|
||||
peg$currPos = end;
|
||||
return true;
|
||||
}
|
||||
} { proxy.username = $.username; }
|
||||
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
|
||||
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
|
||||
|
||||
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
|
||||
|
||||
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
|
||||
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
|
||||
obfs_ssr_param = comma "obfs-param" equals match:$[^,]+ { proxy["obfs-param"] = match; }
|
||||
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; }
|
||||
obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
|
||||
uri = $[^,]+
|
||||
|
||||
transport = comma "transport" equals type:("tcp"/"ws"/"http") { transport.type = type; }
|
||||
transport_host = comma "host" equals host:domain { transport.host = host; }
|
||||
transport_path = comma "path" equals path:uri { transport.path = path; }
|
||||
|
||||
ssr_protocol = comma "protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; }
|
||||
ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
|
||||
|
||||
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; }
|
||||
tls_name = comma sni:("tls-name") equals host:domain { proxy.sni = host; }
|
||||
sni = comma sni:("sni") equals host:domain { proxy.sni = host; }
|
||||
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'); }
|
||||
|
||||
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = 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; }
|
||||
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'; }
|
||||
|
||||
block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
206
backend/src/core/proxy-utils/parsers/peggy/qx.js
Normal file
206
backend/src/core/proxy-utils/parsers/peggy/qx.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import * as peggy from 'peggy';
|
||||
const grammars = String.raw`
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
}}
|
||||
|
||||
// per-parse initializer
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const $ = {};
|
||||
|
||||
function handleObfs() {
|
||||
if (obfs.type === "ws" || obfs.type === "wss") {
|
||||
proxy.network = "ws";
|
||||
if (obfs.type === 'wss') {
|
||||
proxy.tls = true;
|
||||
}
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers.Host", obfs.host);
|
||||
} else if (obfs.type === "over-tls") {
|
||||
proxy.tls = true;
|
||||
} else if (obfs.type === "http") {
|
||||
proxy.network = "http";
|
||||
$set(proxy, "http-opts.path", obfs.path);
|
||||
$set(proxy, "http-opts.headers.Host", obfs.host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan" equals address
|
||||
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
shadowsocks = "shadowsocks" equals address
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/others)* {
|
||||
if (proxy.protocol || proxy.type === "ssr") {
|
||||
proxy.type = "ssr";
|
||||
if (!proxy.protocol) {
|
||||
proxy.protocol = "origin";
|
||||
}
|
||||
// handle ssr obfs
|
||||
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
||||
if (obfs.type) proxy.obfs = obfs.type;
|
||||
} else {
|
||||
proxy.type = "ss";
|
||||
// handle ss obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
proxy.plugin = "obfs";
|
||||
$set(proxy, "plugin-opts", {
|
||||
mode: obfs.type
|
||||
});
|
||||
} else if (obfs.type === "ws" || obfs.type === "wss") {
|
||||
proxy.plugin = "v2ray-plugin";
|
||||
$set(proxy, "plugin-opts.mode", "websocket");
|
||||
if (obfs.type === "wss") {
|
||||
$set(proxy, "plugin-opts.tls", true);
|
||||
}
|
||||
} else if (obfs.type === 'over-tls') {
|
||||
throw new Error('ss over-tls is not supported');
|
||||
}
|
||||
if (obfs.type) {
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vmess = "vmess" 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 = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead === false) {
|
||||
proxy.alterId = 1;
|
||||
} else {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
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
|
||||
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
socks5 = "socks5" equals address
|
||||
(username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
address = server:server ":" port:port {
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
}
|
||||
server = ip/domain
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let end;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
if (input[j] === ":") end = j;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = end || j;
|
||||
$.ip = input.substring(start, end).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
|
||||
password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
|
||||
uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
|
||||
|
||||
method = comma "method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
};
|
||||
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"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
|
||||
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||
udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); }
|
||||
udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
|
||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||
tls_host = comma "tls-host" equals sni:domain { proxy.sni = sni; }
|
||||
tls_verification = comma "tls-verification" equals flag:bool {
|
||||
proxy["skip-cert-verify"] = !flag;
|
||||
}
|
||||
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
|
||||
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
|
||||
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
|
||||
proxy["tls-no-session-ticket"] = flag;
|
||||
}
|
||||
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
||||
proxy["tls-no-session-reuse"] = flag;
|
||||
}
|
||||
|
||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; 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_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
|
||||
|
||||
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
|
||||
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
|
||||
|
||||
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
|
||||
|
||||
uri = $[^,]+
|
||||
|
||||
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
`;
|
||||
let parser;
|
||||
export default function getParser() {
|
||||
if (!parser) {
|
||||
parser = peggy.generate(grammars);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
196
backend/src/core/proxy-utils/parsers/peggy/qx.peg
Normal file
196
backend/src/core/proxy-utils/parsers/peggy/qx.peg
Normal file
@@ -0,0 +1,196 @@
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
}}
|
||||
|
||||
// per-parse initializer
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const $ = {};
|
||||
|
||||
function handleObfs() {
|
||||
if (obfs.type === "ws" || obfs.type === "wss") {
|
||||
proxy.network = "ws";
|
||||
if (obfs.type === 'wss') {
|
||||
proxy.tls = true;
|
||||
}
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers.Host", obfs.host);
|
||||
} else if (obfs.type === "over-tls") {
|
||||
proxy.tls = true;
|
||||
} else if (obfs.type === "http") {
|
||||
proxy.network = "http";
|
||||
$set(proxy, "http-opts.path", obfs.path);
|
||||
$set(proxy, "http-opts.headers.Host", obfs.host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan" equals address
|
||||
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
|
||||
proxy.type = "trojan";
|
||||
handleObfs();
|
||||
}
|
||||
|
||||
shadowsocks = "shadowsocks" equals address
|
||||
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/others)* {
|
||||
if (proxy.protocol || proxy.type === "ssr") {
|
||||
proxy.type = "ssr";
|
||||
if (!proxy.protocol) {
|
||||
proxy.protocol = "origin";
|
||||
}
|
||||
// handle ssr obfs
|
||||
if (obfs.host) proxy["obfs-param"] = obfs.host;
|
||||
if (obfs.type) proxy.obfs = obfs.type;
|
||||
} else {
|
||||
proxy.type = "ss";
|
||||
// handle ss obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
proxy.plugin = "obfs";
|
||||
$set(proxy, "plugin-opts", {
|
||||
mode: obfs.type
|
||||
});
|
||||
} else if (obfs.type === "ws" || obfs.type === "wss") {
|
||||
proxy.plugin = "v2ray-plugin";
|
||||
$set(proxy, "plugin-opts.mode", "websocket");
|
||||
if (obfs.type === "wss") {
|
||||
$set(proxy, "plugin-opts.tls", true);
|
||||
}
|
||||
} else if (obfs.type === 'over-tls') {
|
||||
throw new Error('ss over-tls is not supported');
|
||||
}
|
||||
if (obfs.type) {
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$set(proxy, "plugin-opts.path", obfs.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vmess = "vmess" 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 = "vmess";
|
||||
proxy.cipher = proxy.cipher || "none";
|
||||
if (proxy.aead === false) {
|
||||
proxy.alterId = 1;
|
||||
} else {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
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
|
||||
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
|
||||
proxy.type = "http";
|
||||
}
|
||||
|
||||
socks5 = "socks5" equals address
|
||||
(username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
|
||||
proxy.type = "socks5";
|
||||
}
|
||||
|
||||
address = server:server ":" port:port {
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
}
|
||||
server = ip/domain
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let end;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
if (input[j] === ":") end = j;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = end || j;
|
||||
$.ip = input.substring(start, end).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
|
||||
password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
|
||||
uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
|
||||
|
||||
method = comma "method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
};
|
||||
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"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
|
||||
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||
udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); }
|
||||
udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } }
|
||||
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
|
||||
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
|
||||
tls_host = comma "tls-host" equals sni:domain { proxy.sni = sni; }
|
||||
tls_verification = comma "tls-verification" equals flag:bool {
|
||||
proxy["skip-cert-verify"] = !flag;
|
||||
}
|
||||
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
|
||||
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
|
||||
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
|
||||
proxy["tls-no-session-ticket"] = flag;
|
||||
}
|
||||
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
|
||||
proxy["tls-no-session-reuse"] = flag;
|
||||
}
|
||||
|
||||
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
|
||||
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { proxy.type = "ssr"; 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_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
|
||||
|
||||
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
|
||||
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
|
||||
|
||||
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
|
||||
|
||||
uri = $[^,]+
|
||||
|
||||
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
267
backend/src/core/proxy-utils/parsers/peggy/surge.js
Normal file
267
backend/src/core/proxy-utils/parsers/peggy/surge.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import * as peggy from 'peggy';
|
||||
const grammars = String.raw`
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
}}
|
||||
|
||||
// per-parser initializer
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const $ = {};
|
||||
|
||||
function handleWebsocket() {
|
||||
if (obfs.type === "ws") {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
|
||||
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
|
||||
}
|
||||
}
|
||||
}
|
||||
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/ssh/direct) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
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";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
proxy.plugin = "obfs";
|
||||
$set(proxy, "plugin-opts.mode", obfs.type);
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$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/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.cipher = proxy.cipher || "none";
|
||||
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
proxy.alterId = 1;
|
||||
}
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
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.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
$set(proxy, "obfs-opts.mode", obfs.type);
|
||||
$set(proxy, "obfs-opts.host", obfs.host);
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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.version = 5;
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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.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 {
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
}
|
||||
|
||||
server = ip/domain
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = j;
|
||||
$.ip = input.substring(start, j).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
let start, end;
|
||||
let first = true;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ',') {
|
||||
if (first) {
|
||||
start = j + 1;
|
||||
first = false;
|
||||
} else {
|
||||
end = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
j++;
|
||||
}
|
||||
const match = input.substring(start, end);
|
||||
if (match.indexOf("=") === -1) {
|
||||
$.username = match;
|
||||
peg$currPos = end;
|
||||
return true;
|
||||
}
|
||||
} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
|
||||
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
||||
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_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||
|
||||
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||
|
||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
method = comma "encrypt-method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
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_headers = comma "ws-headers" equals headers:$[^,]+ {
|
||||
const pairs = headers.split("|");
|
||||
const result = {};
|
||||
pairs.forEach(pair => {
|
||||
const [key, value] = pair.trim().split(":");
|
||||
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
|
||||
})
|
||||
obfs["ws-headers"] = result;
|
||||
}
|
||||
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
|
||||
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
|
||||
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
||||
uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
|
||||
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
|
||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||
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(""); }
|
||||
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("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
`;
|
||||
let parser;
|
||||
export default function getParser() {
|
||||
if (!parser) {
|
||||
parser = peggy.generate(grammars);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
257
backend/src/core/proxy-utils/parsers/peggy/surge.peg
Normal file
257
backend/src/core/proxy-utils/parsers/peggy/surge.peg
Normal file
@@ -0,0 +1,257 @@
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
}}
|
||||
|
||||
// per-parser initializer
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const $ = {};
|
||||
|
||||
function handleWebsocket() {
|
||||
if (obfs.type === "ws") {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", obfs.path);
|
||||
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
|
||||
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
|
||||
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
|
||||
}
|
||||
}
|
||||
}
|
||||
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/ssh/direct) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
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";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
proxy.plugin = "obfs";
|
||||
$set(proxy, "plugin-opts.mode", obfs.type);
|
||||
$set(proxy, "plugin-opts.host", obfs.host);
|
||||
$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/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.cipher = proxy.cipher || "none";
|
||||
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
} else {
|
||||
proxy.alterId = 1;
|
||||
}
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleWebsocket();
|
||||
handleShadowTLS();
|
||||
}
|
||||
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.tls = true;
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
// handle obfs
|
||||
if (obfs.type == "http" || obfs.type === "tls") {
|
||||
$set(proxy, "obfs-opts.mode", obfs.type);
|
||||
$set(proxy, "obfs-opts.host", obfs.host);
|
||||
$set(proxy, "obfs-opts.path", obfs.path);
|
||||
}
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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.version = 5;
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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";
|
||||
handleShadowTLS();
|
||||
}
|
||||
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.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 {
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
}
|
||||
|
||||
server = ip/domain
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = j;
|
||||
$.ip = input.substring(start, j).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
|
||||
|
||||
username = & {
|
||||
let j = peg$currPos;
|
||||
let start, end;
|
||||
let first = true;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ',') {
|
||||
if (first) {
|
||||
start = j + 1;
|
||||
first = false;
|
||||
} else {
|
||||
end = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
j++;
|
||||
}
|
||||
const match = input.substring(start, end);
|
||||
if (match.indexOf("=") === -1) {
|
||||
$.username = match;
|
||||
peg$currPos = end;
|
||||
return true;
|
||||
}
|
||||
} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
|
||||
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
|
||||
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_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
|
||||
|
||||
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
|
||||
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
|
||||
|
||||
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
|
||||
|
||||
method = comma "encrypt-method" equals cipher:cipher {
|
||||
proxy.cipher = cipher;
|
||||
}
|
||||
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_headers = comma "ws-headers" equals headers:$[^,]+ {
|
||||
const pairs = headers.split("|");
|
||||
const result = {};
|
||||
pairs.forEach(pair => {
|
||||
const [key, value] = pair.trim().split(":");
|
||||
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
|
||||
})
|
||||
obfs["ws-headers"] = result;
|
||||
}
|
||||
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
|
||||
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
|
||||
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
|
||||
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
|
||||
uri = $[^,]+
|
||||
|
||||
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
|
||||
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
|
||||
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
|
||||
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
|
||||
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
|
||||
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
|
||||
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
|
||||
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
|
||||
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
|
||||
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
|
||||
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
|
||||
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(""); }
|
||||
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("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
|
||||
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
|
||||
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
|
||||
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
|
||||
|
||||
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
|
||||
comma = _ "," _
|
||||
equals = _ "=" _
|
||||
_ = [ \r\t]*
|
||||
bool = b:("true"/"false") { return b === "true" }
|
||||
others = comma [^=,]+ equals [^=,]+
|
||||
168
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js
Normal file
168
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as peggy from 'peggy';
|
||||
const grammars = String.raw`
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
|
||||
function toBool(str) {
|
||||
if (typeof str === 'undefined' || str === null) return undefined;
|
||||
return /(TRUE)|1/i.test(str);
|
||||
}
|
||||
}}
|
||||
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const $ = {};
|
||||
const params = {};
|
||||
}
|
||||
|
||||
start = (trojan) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
|
||||
proxy.type = "trojan";
|
||||
proxy.password = password;
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
proxy.name = name;
|
||||
|
||||
// name may be empty
|
||||
if (!proxy.name) {
|
||||
proxy.name = server + ":" + port;
|
||||
}
|
||||
};
|
||||
|
||||
password = match:$[^@]+ {
|
||||
return decodeURIComponent(match);
|
||||
};
|
||||
|
||||
server = ip/domain;
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let end;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
if (input[j] === ":") end = j;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = end || j;
|
||||
$.ip = input.substring(start, end).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
} else {
|
||||
throw new Error("Invalid port: " + port);
|
||||
}
|
||||
}
|
||||
|
||||
params = "?" head:param tail:("&"@param)* {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
params[key] = decodeURIComponent(value);
|
||||
}
|
||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||
proxy.sni = params["sni"] || params["peer"];
|
||||
proxy['client-fingerprint'] = params.fp;
|
||||
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
|
||||
|
||||
if (toBool(params["ws"])) {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", params["wspath"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
let httpupgrade
|
||||
proxy.network = params["type"]
|
||||
if(proxy.network === 'httpupgrade') {
|
||||
proxy.network = 'ws'
|
||||
httpupgrade = true
|
||||
}
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy.udp = toBool(params["udp"]);
|
||||
proxy.tfo = toBool(params["tfo"]);
|
||||
}
|
||||
|
||||
param = kv/single;
|
||||
|
||||
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
single = key:$[a-z]i+ {
|
||||
params[key] = true;
|
||||
};
|
||||
|
||||
name = "#" + match:$.* {
|
||||
return decodeURIComponent(match);
|
||||
}
|
||||
`;
|
||||
let parser;
|
||||
export default function getParser() {
|
||||
if (!parser) {
|
||||
parser = peggy.generate(grammars);
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
158
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.peg
Normal file
158
backend/src/core/proxy-utils/parsers/peggy/trojan-uri.peg
Normal file
@@ -0,0 +1,158 @@
|
||||
// global initializer
|
||||
{{
|
||||
function $set(obj, path, value) {
|
||||
if (Object(obj) !== obj) return obj;
|
||||
if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || [];
|
||||
path
|
||||
.slice(0, -1)
|
||||
.reduce((a, c, i) => (Object(a[c]) === a[c] ? a[c] : (a[c] = Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] : {})), obj)[
|
||||
path[path.length - 1]
|
||||
] = value;
|
||||
return obj;
|
||||
}
|
||||
|
||||
function toBool(str) {
|
||||
if (typeof str === 'undefined' || str === null) return undefined;
|
||||
return /(TRUE)|1/i.test(str);
|
||||
}
|
||||
}}
|
||||
|
||||
{
|
||||
const proxy = {};
|
||||
const obfs = {};
|
||||
const $ = {};
|
||||
const params = {};
|
||||
}
|
||||
|
||||
start = (trojan) {
|
||||
return proxy
|
||||
}
|
||||
|
||||
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
|
||||
proxy.type = "trojan";
|
||||
proxy.password = password;
|
||||
proxy.server = server;
|
||||
proxy.port = port;
|
||||
proxy.name = name;
|
||||
|
||||
// name may be empty
|
||||
if (!proxy.name) {
|
||||
proxy.name = server + ":" + port;
|
||||
}
|
||||
};
|
||||
|
||||
password = match:$[^@]+ {
|
||||
return decodeURIComponent(match);
|
||||
};
|
||||
|
||||
server = ip/domain;
|
||||
|
||||
domain = match:[0-9a-zA-z-_.]+ {
|
||||
const domain = match.join("");
|
||||
if (/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
|
||||
ip = & {
|
||||
const start = peg$currPos;
|
||||
let end;
|
||||
let j = start;
|
||||
while (j < input.length) {
|
||||
if (input[j] === ",") break;
|
||||
if (input[j] === ":") end = j;
|
||||
j++;
|
||||
}
|
||||
peg$currPos = end || j;
|
||||
$.ip = input.substring(start, end).trim();
|
||||
return true;
|
||||
} { return $.ip; }
|
||||
|
||||
port = digits:[0-9]+ {
|
||||
const port = parseInt(digits.join(""), 10);
|
||||
if (port >= 0 && port <= 65535) {
|
||||
return port;
|
||||
} else {
|
||||
throw new Error("Invalid port: " + port);
|
||||
}
|
||||
}
|
||||
|
||||
params = "?" head:param tail:("&"@param)* {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
params[key] = decodeURIComponent(value);
|
||||
}
|
||||
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
|
||||
proxy.sni = params["sni"] || params["peer"];
|
||||
proxy['client-fingerprint'] = params.fp;
|
||||
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
|
||||
|
||||
if (toBool(params["ws"])) {
|
||||
proxy.network = "ws";
|
||||
$set(proxy, "ws-opts.path", params["wspath"]);
|
||||
}
|
||||
|
||||
if (params["type"]) {
|
||||
let httpupgrade
|
||||
proxy.network = params["type"]
|
||||
if(proxy.network === 'httpupgrade') {
|
||||
proxy.network = 'ws'
|
||||
httpupgrade = true
|
||||
}
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy.udp = toBool(params["udp"]);
|
||||
proxy.tfo = toBool(params["tfo"]);
|
||||
}
|
||||
|
||||
param = kv/single;
|
||||
|
||||
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
single = key:$[a-z]i+ {
|
||||
params[key] = true;
|
||||
};
|
||||
|
||||
name = "#" + match:$.* {
|
||||
return decodeURIComponent(match);
|
||||
}
|
||||
193
backend/src/core/proxy-utils/preprocessors/index.js
Normal file
193
backend/src/core/proxy-utils/preprocessors/index.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import { safeLoad } from '@/utils/yaml';
|
||||
import { Base64 } from 'js-base64';
|
||||
import $ from '@/core/app';
|
||||
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||
// simply discard HTML
|
||||
const parse = () => '';
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Base64Encoded() {
|
||||
const name = 'Base64 Pre-processor';
|
||||
|
||||
const keys = [
|
||||
'dm1lc3M', // vmess
|
||||
'c3NyOi8v', // ssr://
|
||||
'c29ja3M6Ly', // socks://
|
||||
'dHJvamFu', // trojan
|
||||
'c3M6Ly', // ss:/
|
||||
'c3NkOi8v', // ssd://
|
||||
'c2hhZG93', // shadow
|
||||
'aHR0c', // htt
|
||||
'dmxlc3M=', // vless
|
||||
'aHlzdGVyaWEy', // hysteria2
|
||||
'aHkyOi8v', // hy2://
|
||||
'd2lyZWd1YXJkOi8v', // wireguard://
|
||||
'd2c6Ly8=', // wg://
|
||||
'dHVpYzovLw==', // tuic://
|
||||
];
|
||||
|
||||
const test = function (raw) {
|
||||
return (
|
||||
!/^\w+:\/\/\w+/im.test(raw) &&
|
||||
keys.some((k) => raw.indexOf(k) !== -1)
|
||||
);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
const decoded = Base64.decode(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 };
|
||||
}
|
||||
|
||||
function fallbackBase64Encoded() {
|
||||
const name = 'Fallback Base64 Pre-processor';
|
||||
|
||||
const test = function (raw) {
|
||||
return true;
|
||||
};
|
||||
const parse = function (raw) {
|
||||
const decoded = Base64.decode(raw);
|
||||
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
|
||||
$.error(
|
||||
`Fallback Base64 Pre-processor error: decoded line does not start with protocol`,
|
||||
);
|
||||
return raw;
|
||||
}
|
||||
|
||||
return decoded;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function Clash() {
|
||||
const name = 'Clash Pre-processor';
|
||||
const test = function (raw) {
|
||||
if (!/proxies/.test(raw)) return false;
|
||||
const content = safeLoad(raw);
|
||||
return content.proxies && Array.isArray(content.proxies);
|
||||
};
|
||||
const parse = function (raw, includeProxies) {
|
||||
// Clash YAML format
|
||||
|
||||
// 防止 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 if (['null'].includes(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 };
|
||||
}
|
||||
|
||||
function SSD() {
|
||||
const name = 'SSD Pre-processor';
|
||||
const test = function (raw) {
|
||||
return raw.indexOf('ssd://') === 0;
|
||||
};
|
||||
const parse = function (raw) {
|
||||
// preprocessing for SSD subscription format
|
||||
const output = [];
|
||||
let ssdinfo = JSON.parse(Base64.decode(raw.split('ssd://')[1]));
|
||||
let port = ssdinfo.port;
|
||||
let method = ssdinfo.encryption;
|
||||
let password = ssdinfo.password;
|
||||
// servers config
|
||||
let servers = ssdinfo.servers;
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
let server = servers[i];
|
||||
method = server.encryption ? server.encryption : method;
|
||||
password = server.password ? server.password : password;
|
||||
let userinfo = Base64.encode(method + ':' + password);
|
||||
let hostname = server.server;
|
||||
port = server.port ? server.port : port;
|
||||
let tag = server.remarks ? server.remarks : i;
|
||||
let plugin = server.plugin_options
|
||||
? '/?plugin=' +
|
||||
encodeURIComponent(
|
||||
server.plugin + ';' + server.plugin_options,
|
||||
)
|
||||
: '';
|
||||
output[i] =
|
||||
'ss://' +
|
||||
userinfo +
|
||||
'@' +
|
||||
hostname +
|
||||
':' +
|
||||
port +
|
||||
plugin +
|
||||
'#' +
|
||||
tag;
|
||||
}
|
||||
return output.join('\n');
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function FullConfig() {
|
||||
const name = 'Full Config Preprocessor';
|
||||
const test = function (raw) {
|
||||
return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
|
||||
};
|
||||
const parse = function (raw) {
|
||||
const match = raw.match(
|
||||
/^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
|
||||
)?.[1];
|
||||
return match || raw;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [
|
||||
HTML(),
|
||||
Clash(),
|
||||
Base64Encoded(),
|
||||
SSD(),
|
||||
FullConfig(),
|
||||
fallbackBase64Encoded(),
|
||||
];
|
||||
1208
backend/src/core/proxy-utils/processors/index.js
Normal file
1208
backend/src/core/proxy-utils/processors/index.js
Normal file
File diff suppressed because it is too large
Load Diff
206
backend/src/core/proxy-utils/producers/clash.js
Normal file
206
backend/src/core/proxy-utils/producers/clash.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function Clash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
// VLESS XTLS is not supported by Clash
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
|
||||
// github.com/Dreamacro/clash/pull/2891/files
|
||||
// filter unsupported proxies
|
||||
// https://clash.wiki/configuration/outbound.html#shadowsocks
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'vless',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'wireguard',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'ss' &&
|
||||
![
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'rc4-md5',
|
||||
'chacha20-ietf',
|
||||
'xchacha20',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
].includes(proxy.cipher)) ||
|
||||
(proxy.type === 'snell' && proxy.version >= 4) ||
|
||||
(proxy.type === 'vless' &&
|
||||
(typeof proxy.flow !== 'undefined' ||
|
||||
proxy['reality-opts']))
|
||||
) {
|
||||
return false;
|
||||
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
|
||||
$.error(
|
||||
`Clash 不支持前置代理字段. 已过滤节点 ${proxy.name}`,
|
||||
);
|
||||
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://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 (
|
||||
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.network === 'ws') {
|
||||
const wsPath = proxy['ws-opts']?.path;
|
||||
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, path = '', ed = ''] = reg.exec(wsPath);
|
||||
proxy['ws-opts'].path = path;
|
||||
if (ed !== '') {
|
||||
proxy['ws-opts']['early-data-header-name'] =
|
||||
'Sec-WebSocket-Protocol';
|
||||
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
|
||||
}
|
||||
}
|
||||
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 (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 (
|
||||
['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 };
|
||||
}
|
||||
281
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
281
backend/src/core/proxy-utils/producers/clashmeta.js
Normal file
@@ -0,0 +1,281 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
|
||||
const ipVersions = {
|
||||
dual: 'dual',
|
||||
'v4-only': 'ipv4',
|
||||
'v6-only': 'ipv6',
|
||||
'prefer-v4': 'ipv4-prefer',
|
||||
'prefer-v6': 'ipv6-prefer',
|
||||
};
|
||||
|
||||
export default function ClashMeta_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (proxy.type === 'snell' && proxy.version >= 4) {
|
||||
return false;
|
||||
} else if (['juicity'].includes(proxy.type)) {
|
||||
return false;
|
||||
} else if (
|
||||
['ss'].includes(proxy.type) &&
|
||||
![
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'aes-128-ccm',
|
||||
'aes-192-ccm',
|
||||
'aes-256-ccm',
|
||||
'aes-128-gcm-siv',
|
||||
'aes-256-gcm-siv',
|
||||
'chacha20-ietf',
|
||||
'chacha20',
|
||||
'xchacha20',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'chacha8-ietf-poly1305',
|
||||
'xchacha8-ietf-poly1305',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305',
|
||||
'lea-128-gcm',
|
||||
'lea-192-gcm',
|
||||
'lea-256-gcm',
|
||||
'rabbit128-poly1305',
|
||||
'aegis-128l',
|
||||
'aegis-256',
|
||||
'aez-384',
|
||||
'deoxys-ii-256-128',
|
||||
'rc4-md5',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
// https://wiki.metacubex.one/config/proxies/ss/#cipher
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'none',
|
||||
'zero',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'tuic') {
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
} else {
|
||||
proxy.alpn = ['h3'];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
proxy.version = 5;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === '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 (
|
||||
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.network === 'ws') {
|
||||
const wsPath = proxy['ws-opts']?.path;
|
||||
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, path = '', ed = ''] = reg.exec(wsPath);
|
||||
proxy['ws-opts'].path = path;
|
||||
if (ed !== '') {
|
||||
proxy['ws-opts']['early-data-header-name'] =
|
||||
'Sec-WebSocket-Protocol';
|
||||
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
|
||||
}
|
||||
}
|
||||
|
||||
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' || opts['delete-underscore-fields']) {
|
||||
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'];
|
||||
}
|
||||
|
||||
if (proxy['ip-version']) {
|
||||
proxy['ip-version'] =
|
||||
ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
}
|
||||
return proxy;
|
||||
});
|
||||
|
||||
return type === 'internal'
|
||||
? list
|
||||
: 'proxies:\n' +
|
||||
list
|
||||
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
|
||||
.join('');
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
428
backend/src/core/proxy-utils/producers/egern.js
Normal file
428
backend/src/core/proxy-utils/producers/egern.js
Normal file
@@ -0,0 +1,428 @@
|
||||
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 (original.plugin === 'obfs') {
|
||||
proxy.obfs = original['plugin-opts'].mode;
|
||||
proxy.obfs_host = original['plugin-opts'].host;
|
||||
proxy.obfs_uri = original['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 (
|
||||
original['obfs-password'] &&
|
||||
original.obfs == 'salamander'
|
||||
) {
|
||||
proxy.obfs = 'salamander';
|
||||
proxy.obfs_password = original['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,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (
|
||||
['ss'].includes(original.type) &&
|
||||
proxy.shadow_tls &&
|
||||
original['udp-port'] > 0 &&
|
||||
original['udp-port'] <= 65535
|
||||
) {
|
||||
proxy['udp_port'] = original['udp-port'];
|
||||
}
|
||||
|
||||
delete proxy.subName;
|
||||
delete proxy.collectionName;
|
||||
delete proxy.id;
|
||||
delete proxy.resolved;
|
||||
delete proxy['no-resolve'];
|
||||
|
||||
if (proxy.transport) {
|
||||
for (const key in proxy.transport) {
|
||||
if (
|
||||
Object.keys(proxy.transport[key]).length === 0 ||
|
||||
Object.values(proxy.transport[key]).every(
|
||||
(v) => v == null,
|
||||
)
|
||||
) {
|
||||
delete proxy.transport[key];
|
||||
}
|
||||
}
|
||||
if (Object.keys(proxy.transport).length === 0) {
|
||||
delete proxy.transport;
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
56
backend/src/core/proxy-utils/producers/index.js
Normal file
56
backend/src/core/proxy-utils/producers/index.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Surge_Producer from './surge';
|
||||
import SurgeMac_Producer from './surgemac';
|
||||
import Clash_Producer from './clash';
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import Stash_Producer from './stash';
|
||||
import Loon_Producer from './loon';
|
||||
import URI_Producer from './uri';
|
||||
import V2Ray_Producer from './v2ray';
|
||||
import QX_Producer from './qx';
|
||||
import Shadowrocket_Producer from './shadowrocket';
|
||||
import Surfboard_Producer from './surfboard';
|
||||
import singbox_Producer from './sing-box';
|
||||
import Egern_Producer from './egern';
|
||||
|
||||
function JSON_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type) =>
|
||||
type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);
|
||||
return { type, produce };
|
||||
}
|
||||
|
||||
export default {
|
||||
qx: QX_Producer(),
|
||||
QX: QX_Producer(),
|
||||
QuantumultX: QX_Producer(),
|
||||
surge: Surge_Producer(),
|
||||
Surge: Surge_Producer(),
|
||||
SurgeMac: SurgeMac_Producer(),
|
||||
Loon: Loon_Producer(),
|
||||
Clash: Clash_Producer(),
|
||||
meta: ClashMeta_Producer(),
|
||||
clashmeta: ClashMeta_Producer(),
|
||||
'clash.meta': ClashMeta_Producer(),
|
||||
'Clash.Meta': ClashMeta_Producer(),
|
||||
ClashMeta: ClashMeta_Producer(),
|
||||
mihomo: ClashMeta_Producer(),
|
||||
Mihomo: ClashMeta_Producer(),
|
||||
uri: URI_Producer(),
|
||||
URI: URI_Producer(),
|
||||
v2: V2Ray_Producer(),
|
||||
v2ray: V2Ray_Producer(),
|
||||
V2Ray: V2Ray_Producer(),
|
||||
json: JSON_Producer(),
|
||||
JSON: JSON_Producer(),
|
||||
stash: Stash_Producer(),
|
||||
Stash: Stash_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(),
|
||||
};
|
||||
693
backend/src/core/proxy-utils/producers/loon.js
Normal file
693
backend/src/core/proxy-utils/producers/loon.js
Normal file
@@ -0,0 +1,693 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
const targetPlatform = 'Loon';
|
||||
import { isPresent, Result } 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() {
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
case 'trojan':
|
||||
return trojan(proxy);
|
||||
case 'vmess':
|
||||
return vmess(proxy, opts['include-unsupported-proxy']);
|
||||
case 'vless':
|
||||
return vless(proxy, opts['include-unsupported-proxy']);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'wireguard':
|
||||
return wireguard(proxy);
|
||||
case 'hysteria2':
|
||||
return hysteria2(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocks(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(
|
||||
`${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
||||
);
|
||||
|
||||
// obfs
|
||||
if (isPresent(proxy, 'plugin')) {
|
||||
if (proxy.plugin === 'obfs') {
|
||||
result.append(`,obfs-name=${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 if (!['shadow-tls'].includes(proxy.plugin)) {
|
||||
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
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
function shadowsocksr(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=shadowsocksr,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
|
||||
);
|
||||
|
||||
// ssr protocol
|
||||
result.append(`,protocol=${proxy.protocol}`);
|
||||
result.appendIfPresent(
|
||||
`,protocol-param=${proxy['protocol-param']}`,
|
||||
'protocol-param',
|
||||
);
|
||||
|
||||
// obfs
|
||||
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
|
||||
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
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
function trojan(proxy) {
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
|
||||
);
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
// transport
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// 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
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
function vmess(proxy) {
|
||||
const isReality = !!proxy['reality-opts'];
|
||||
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
|
||||
);
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
// transport
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else if (proxy.network === 'http') {
|
||||
result.append(`,transport=http`);
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
result.appendIfPresent(
|
||||
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
|
||||
'http-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
|
||||
'http-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
} else {
|
||||
result.append(`,transport=tcp`);
|
||||
}
|
||||
|
||||
// tls
|
||||
result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
if (isReality) {
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,public-key="${proxy['reality-opts']['public-key']}"`,
|
||||
'reality-opts.public-key',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,short-id=${proxy['reality-opts']['short-id']}`,
|
||||
'reality-opts.short-id',
|
||||
);
|
||||
} else {
|
||||
// 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
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
result.append(`,alterId=${proxy.aead ? 0 : 1}`);
|
||||
} else {
|
||||
result.append(`,alterId=${proxy.alterId}`);
|
||||
}
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
function vless(proxy) {
|
||||
let isXtls = false;
|
||||
const isReality = !!proxy['reality-opts'];
|
||||
|
||||
if (typeof proxy.flow !== 'undefined') {
|
||||
if (['xtls-rprx-vision'].includes(proxy.flow)) {
|
||||
isXtls = true;
|
||||
} else {
|
||||
throw new Error(`VLESS flow(${proxy.flow}) is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = new Result(proxy);
|
||||
result.append(
|
||||
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
|
||||
);
|
||||
if (proxy.network === 'tcp') {
|
||||
delete proxy.network;
|
||||
}
|
||||
// transport
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
result.append(`,transport=ws`);
|
||||
result.appendIfPresent(
|
||||
`,path=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else if (proxy.network === 'http') {
|
||||
result.append(`,transport=http`);
|
||||
let httpPath = proxy['http-opts']?.path;
|
||||
let httpHost = proxy['http-opts']?.headers?.Host;
|
||||
result.appendIfPresent(
|
||||
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
|
||||
'http-opts.path',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
|
||||
'http-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
} else {
|
||||
result.append(`,transport=tcp`);
|
||||
}
|
||||
|
||||
// tls
|
||||
result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
if (isXtls) {
|
||||
result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');
|
||||
}
|
||||
if (isReality) {
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,public-key="${proxy['reality-opts']['public-key']}"`,
|
||||
'reality-opts.public-key',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,short-id=${proxy['reality-opts']['short-id']}`,
|
||||
'reality-opts.short-id',
|
||||
);
|
||||
} else {
|
||||
// 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
|
||||
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
|
||||
|
||||
// tls verification
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
|
||||
'skip-cert-verify',
|
||||
);
|
||||
|
||||
// tfo
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
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
|
||||
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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(`${proxy.name}=wireguard`);
|
||||
|
||||
result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
|
||||
result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
|
||||
|
||||
result.appendIfPresent(
|
||||
`,private-key="${proxy['private-key']}"`,
|
||||
'private-key',
|
||||
);
|
||||
result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
|
||||
|
||||
if (proxy.dns) {
|
||||
if (Array.isArray(proxy.dns)) {
|
||||
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
|
||||
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(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
|
||||
result.appendIfPresent(
|
||||
`,keepalive=${proxy['persistent-keepalive']}`,
|
||||
'persistent-keepalive',
|
||||
);
|
||||
result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
|
||||
const allowedIps = Array.isArray(proxy['allowed-ips'])
|
||||
? proxy['allowed-ips'].join(',')
|
||||
: proxy['allowed-ips'];
|
||||
let reserved = Array.isArray(proxy.reserved)
|
||||
? proxy.reserved.join(',')
|
||||
: proxy.reserved;
|
||||
if (reserved) {
|
||||
reserved = `,reserved=[${reserved}]`;
|
||||
}
|
||||
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
if (presharedKey) {
|
||||
presharedKey = `,preshared-key="${presharedKey}"`;
|
||||
}
|
||||
result.append(
|
||||
`,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
|
||||
allowedIps ?? '0.0.0.0/0,::/0'
|
||||
}",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
|
||||
presharedKey ?? ''
|
||||
}}]`,
|
||||
);
|
||||
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
|
||||
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function hysteria2(proxy) {
|
||||
if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
|
||||
throw new Error(`only salamander obfs is supported`);
|
||||
}
|
||||
const result = new Result(proxy);
|
||||
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
|
||||
|
||||
result.appendIfPresent(`,"${proxy.password}"`, 'password');
|
||||
|
||||
// sni
|
||||
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
|
||||
result.appendIfPresent(
|
||||
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
|
||||
'tls-fingerprint',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
|
||||
'tls-pubkey-sha256',
|
||||
);
|
||||
result.appendIfPresent(
|
||||
`,skip-cert-verify=${proxy['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');
|
||||
|
||||
// block-quic
|
||||
if (proxy['block-quic'] === 'on') {
|
||||
result.append(',block-quic=true');
|
||||
} else if (proxy['block-quic'] === 'off') {
|
||||
result.append(',block-quic=false');
|
||||
}
|
||||
|
||||
// udp
|
||||
if (proxy.udp) {
|
||||
result.append(`,udp=true`);
|
||||
}
|
||||
|
||||
// download-bandwidth
|
||||
result.appendIfPresent(
|
||||
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
|
||||
'down',
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
602
backend/src/core/proxy-utils/producers/qx.js
Normal file
602
backend/src/core/proxy-utils/producers/qx.js
Normal file
@@ -0,0 +1,602 @@
|
||||
import { isPresent, Result } from './utils';
|
||||
|
||||
const targetPlatform = 'QX';
|
||||
|
||||
export default function QX_Producer() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'ss':
|
||||
return shadowsocks(proxy);
|
||||
case 'ssr':
|
||||
return shadowsocksr(proxy);
|
||||
case 'trojan':
|
||||
return trojan(proxy);
|
||||
case 'vmess':
|
||||
return vmess(proxy);
|
||||
case 'http':
|
||||
return http(proxy);
|
||||
case 'socks5':
|
||||
return socks5(proxy);
|
||||
case 'vless':
|
||||
return vless(proxy);
|
||||
}
|
||||
throw new Error(
|
||||
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
|
||||
);
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
|
||||
function shadowsocks(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.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',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
throw new Error(`cipher ${proxy.cipher} is not supported`);
|
||||
}
|
||||
append(`shadowsocks=${proxy.server}:${proxy.port}`);
|
||||
append(`,method=${proxy.cipher}`);
|
||||
append(`,password=${proxy.password}`);
|
||||
|
||||
// obfs
|
||||
if (needTls(proxy)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
if (isPresent(proxy, 'plugin')) {
|
||||
if (proxy.plugin === 'obfs') {
|
||||
const opts = proxy['plugin-opts'];
|
||||
append(`,obfs=${opts.mode}`);
|
||||
} else if (
|
||||
proxy.plugin === 'v2ray-plugin' &&
|
||||
proxy['plugin-opts'].mode === 'websocket'
|
||||
) {
|
||||
const opts = proxy['plugin-opts'];
|
||||
if (opts.tls) append(`,obfs=wss`);
|
||||
else append(`,obfs=ws`);
|
||||
} else {
|
||||
throw new Error(`plugin is not supported`);
|
||||
}
|
||||
appendIfPresent(
|
||||
`,obfs-host=${proxy['plugin-opts'].host}`,
|
||||
'plugin-opts.host',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-uri=${proxy['plugin-opts'].path}`,
|
||||
'plugin-opts.path',
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
// udp over tcp
|
||||
if (proxy['_ssr_python_uot']) {
|
||||
append(`,udp-over-tcp=true`);
|
||||
} else if (proxy['udp-over-tcp']) {
|
||||
if (
|
||||
!proxy['udp-over-tcp-version'] ||
|
||||
proxy['udp-over-tcp-version'] === 1
|
||||
) {
|
||||
append(`,udp-over-tcp=sp.v1`);
|
||||
} else if (proxy['udp-over-tcp-version'] === 2) {
|
||||
append(`,udp-over-tcp=sp.v2`);
|
||||
}
|
||||
}
|
||||
|
||||
// server_check_url
|
||||
result.appendIfPresent(
|
||||
`,server_check_url=${proxy['test-url']}`,
|
||||
'test-url',
|
||||
);
|
||||
|
||||
// tag
|
||||
append(`,tag=${proxy.name}`);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
function shadowsocksr(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`shadowsocks=${proxy.server}:${proxy.port}`);
|
||||
append(`,method=${proxy.cipher}`);
|
||||
append(`,password=${proxy.password}`);
|
||||
|
||||
// ssr protocol
|
||||
append(`,ssr-protocol=${proxy.protocol}`);
|
||||
appendIfPresent(
|
||||
`,ssr-protocol-param=${proxy['protocol-param']}`,
|
||||
'protocol-param',
|
||||
);
|
||||
|
||||
// obfs
|
||||
appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
|
||||
appendIfPresent(`,obfs-host=${proxy['obfs-param']}`, 'obfs-param');
|
||||
|
||||
// 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 trojan(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`trojan=${proxy.server}:${proxy.port}`);
|
||||
append(`,password=${proxy.password}`);
|
||||
|
||||
// obfs ws
|
||||
if (isPresent(proxy, 'network')) {
|
||||
if (proxy.network === 'ws') {
|
||||
if (needTls(proxy)) append(`,obfs=wss`);
|
||||
else append(`,obfs=ws`);
|
||||
appendIfPresent(
|
||||
`,obfs-uri=${proxy['ws-opts']?.path}`,
|
||||
'ws-opts.path',
|
||||
);
|
||||
appendIfPresent(
|
||||
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
|
||||
'ws-opts.headers.Host',
|
||||
);
|
||||
} else {
|
||||
throw new Error(`network ${proxy.network} is unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
// over tls
|
||||
if (proxy.network !== 'ws' && needTls(proxy)) {
|
||||
append(`,over-tls=true`);
|
||||
}
|
||||
|
||||
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 vmess(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`vmess=${proxy.server}:${proxy.port}`);
|
||||
|
||||
// cipher
|
||||
let cipher;
|
||||
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 {
|
||||
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');
|
||||
}
|
||||
|
||||
// AEAD
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
append(`,aead=${proxy.aead}`);
|
||||
} else {
|
||||
append(`,aead=${proxy.alterId === 0}`);
|
||||
}
|
||||
|
||||
// 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 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)) {
|
||||
if (proxy.tls) append(`,obfs=over-tls`);
|
||||
} 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) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`http=${proxy.server}:${proxy.port}`);
|
||||
appendIfPresent(`,username=${proxy.username}`, 'username');
|
||||
appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
// tls
|
||||
if (needTls(proxy)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
appendIfPresent(`,over-tls=${proxy.tls}`, '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 socks5(proxy) {
|
||||
const result = new Result(proxy);
|
||||
const append = result.append.bind(result);
|
||||
const appendIfPresent = result.appendIfPresent.bind(result);
|
||||
|
||||
append(`socks5=${proxy.server}:${proxy.port}`);
|
||||
appendIfPresent(`,username=${proxy.username}`, 'username');
|
||||
appendIfPresent(`,password=${proxy.password}`, 'password');
|
||||
|
||||
// tls
|
||||
if (needTls(proxy)) {
|
||||
proxy.tls = true;
|
||||
}
|
||||
appendIfPresent(`,over-tls=${proxy.tls}`, '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 needTls(proxy) {
|
||||
return proxy.tls;
|
||||
}
|
||||
244
backend/src/core/proxy-utils/producers/shadowrocket.js
Normal file
244
backend/src/core/proxy-utils/producers/shadowrocket.js
Normal file
@@ -0,0 +1,244 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function Shadowrocket_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
if (opts['include-unsupported-proxy']) return true;
|
||||
if (proxy.type === 'snell' && proxy.version >= 4) {
|
||||
return false;
|
||||
} else if (['mieru'].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 (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'none',
|
||||
'zero',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
].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 === '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 (
|
||||
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.network === 'ws') {
|
||||
const wsPath = proxy['ws-opts']?.path;
|
||||
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, path = '', ed = ''] = reg.exec(wsPath);
|
||||
proxy['ws-opts'].path = path;
|
||||
if (ed !== '') {
|
||||
proxy['ws-opts']['early-data-header-name'] =
|
||||
'Sec-WebSocket-Protocol';
|
||||
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
|
||||
}
|
||||
}
|
||||
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 (
|
||||
['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) => {
|
||||
return ' - ' + JSON.stringify(proxy) + '\n';
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
return { type, produce };
|
||||
}
|
||||
952
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
952
backend/src/core/proxy-utils/producers/sing-box.js
Normal file
@@ -0,0 +1,952 @@
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import $ from '@/core/app';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
|
||||
const ipVersions = {
|
||||
ipv4: 'ipv4_only',
|
||||
ipv6: 'ipv6_only',
|
||||
'v4-only': 'ipv4_only',
|
||||
'v6-only': 'ipv6_only',
|
||||
'ipv4-prefer': 'prefer_ipv4',
|
||||
'ipv6-prefer': 'prefer_ipv6',
|
||||
'prefer-v4': 'prefer_ipv4',
|
||||
'prefer-v6': 'prefer_ipv6',
|
||||
};
|
||||
|
||||
const ipVersionParser = (proxy, parsedProxy) => {
|
||||
const strategy = ipVersions[proxy['ip-version']];
|
||||
if (proxy._dns_server && strategy) {
|
||||
parsedProxy.domain_resolver = {
|
||||
server: proxy._dns_server,
|
||||
strategy,
|
||||
};
|
||||
}
|
||||
};
|
||||
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;
|
||||
if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) {
|
||||
proxy.multiplex.brutal = {
|
||||
enabled: true,
|
||||
};
|
||||
if (smux['brutal-opts']?.up)
|
||||
proxy.multiplex.brutal.up_mbps = parseInt(
|
||||
`${smux['brutal-opts']?.up}`,
|
||||
10,
|
||||
);
|
||||
if (smux['brutal-opts']?.down)
|
||||
proxy.multiplex.brutal.down_mbps = parseInt(
|
||||
`${smux['brutal-opts']?.down}`,
|
||||
10,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const wsParser = (proxy, parsedProxy) => {
|
||||
const transport = { type: 'ws', headers: {} };
|
||||
if (proxy['ws-opts']) {
|
||||
const {
|
||||
path: wsPath = '',
|
||||
headers: wsHeaders = {},
|
||||
'max-early-data': max_early_data,
|
||||
'early-data-header-name': early_data_header_name,
|
||||
} = proxy['ws-opts'];
|
||||
transport.early_data_header_name = early_data_header_name;
|
||||
transport.max_early_data = parseInt(max_early_data, 10);
|
||||
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);
|
||||
ipVersionParser(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);
|
||||
ipVersionParser(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);
|
||||
ipVersionParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
|
||||
const shadowTLSParser = (proxy = {}) => {
|
||||
const ssPart = {
|
||||
tag: proxy.name,
|
||||
type: 'shadowsocks',
|
||||
method: proxy.cipher,
|
||||
password: proxy.password,
|
||||
detour: `${proxy.name}_shadowtls`,
|
||||
};
|
||||
if (proxy.uot) ssPart.udp_over_tcp = true;
|
||||
if (proxy['udp-over-tcp']) {
|
||||
ssPart.udp_over_tcp = {
|
||||
enabled: true,
|
||||
version:
|
||||
!proxy['udp-over-tcp-version'] ||
|
||||
proxy['udp-over-tcp-version'] === 1
|
||||
? 1
|
||||
: 2,
|
||||
};
|
||||
}
|
||||
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);
|
||||
ipVersionParser(proxy, stPart);
|
||||
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 = {
|
||||
enabled: true,
|
||||
version:
|
||||
!proxy['udp-over-tcp-version'] ||
|
||||
proxy['udp-over-tcp-version'] === 1
|
||||
? 1
|
||||
: 2,
|
||||
};
|
||||
}
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
networkParser(proxy, parsedProxy);
|
||||
tfoParser(proxy, parsedProxy);
|
||||
detourParser(proxy, parsedProxy);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
ipVersionParser(proxy, 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);
|
||||
ipVersionParser(proxy, 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);
|
||||
ipVersionParser(proxy, 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.xudp) parsedProxy.packet_encoding = 'xudp';
|
||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||
// if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow;
|
||||
if (proxy.flow != null) parsedProxy.flow = proxy.flow;
|
||||
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);
|
||||
smuxParser(proxy.smux, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
ipVersionParser(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);
|
||||
ipVersionParser(proxy, 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$');
|
||||
// sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps
|
||||
if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) {
|
||||
parsedProxy.up = `${proxy.up}`;
|
||||
} else {
|
||||
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
|
||||
}
|
||||
if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) {
|
||||
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);
|
||||
ipVersionParser(proxy, parsedProxy);
|
||||
return parsedProxy;
|
||||
};
|
||||
const hysteria2Parser = (proxy = {}) => {
|
||||
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 (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);
|
||||
ipVersionParser(proxy, 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);
|
||||
ipVersionParser(proxy, 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`;
|
||||
if (/^\d+$/.test(proxy['min-idle-session']))
|
||||
parsedProxy.min_idle_session = parseInt(
|
||||
`${proxy['min-idle-session']}`,
|
||||
10,
|
||||
);
|
||||
detourParser(proxy, parsedProxy);
|
||||
tlsParser(proxy, parsedProxy);
|
||||
ipVersionParser(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);
|
||||
ipVersionParser(proxy, 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 };
|
||||
}
|
||||
318
backend/src/core/proxy-utils/producers/stash.js
Normal file
318
backend/src/core/proxy-utils/producers/stash.js
Normal file
@@ -0,0 +1,318 @@
|
||||
import { isPresent } from '@/core/proxy-utils/producers/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function Stash_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies, type, opts = {}) => {
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
|
||||
const list = proxies
|
||||
.filter((proxy) => {
|
||||
if (
|
||||
![
|
||||
'ss',
|
||||
'ssr',
|
||||
'vmess',
|
||||
'socks5',
|
||||
'http',
|
||||
'snell',
|
||||
'trojan',
|
||||
'tuic',
|
||||
'vless',
|
||||
'wireguard',
|
||||
'hysteria',
|
||||
'hysteria2',
|
||||
'ssh',
|
||||
'juicity',
|
||||
].includes(proxy.type) ||
|
||||
(proxy.type === 'ss' &&
|
||||
![
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'aes-128-cfb',
|
||||
'aes-192-cfb',
|
||||
'aes-256-cfb',
|
||||
'aes-128-ctr',
|
||||
'aes-192-ctr',
|
||||
'aes-256-ctr',
|
||||
'rc4-md5',
|
||||
'chacha20-ietf',
|
||||
'xchacha20',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
].includes(proxy.cipher)) ||
|
||||
(proxy.type === 'snell' && proxy.version >= 4) ||
|
||||
(proxy.type === 'vless' &&
|
||||
proxy['reality-opts'] &&
|
||||
!['xtls-rprx-vision'].includes(proxy.flow))
|
||||
) {
|
||||
return false;
|
||||
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
|
||||
$.error(
|
||||
`Stash 暂不支持前置代理字段. 已过滤节点 ${proxy.name}. 请使用 代理的转发链 https://stash.wiki/proxy-protocols/proxy-groups#relay`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((proxy) => {
|
||||
if (proxy.type === 'vmess') {
|
||||
// handle vmess aead
|
||||
if (isPresent(proxy, 'aead')) {
|
||||
if (proxy.aead) {
|
||||
proxy.alterId = 0;
|
||||
}
|
||||
delete proxy.aead;
|
||||
}
|
||||
if (isPresent(proxy, 'sni')) {
|
||||
proxy.servername = proxy.sni;
|
||||
delete proxy.sni;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
|
||||
// https://stash.wiki/proxy-protocols/proxy-types#vmess
|
||||
if (
|
||||
isPresent(proxy, 'cipher') &&
|
||||
![
|
||||
'auto',
|
||||
'aes-128-gcm',
|
||||
'chacha20-poly1305',
|
||||
'none',
|
||||
].includes(proxy.cipher)
|
||||
) {
|
||||
proxy.cipher = 'auto';
|
||||
}
|
||||
} else if (proxy.type === 'tuic') {
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
} else {
|
||||
proxy.alpn = ['h3'];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
|
||||
if (
|
||||
(!proxy.token || proxy.token.length === 0) &&
|
||||
!isPresent(proxy, 'version')
|
||||
) {
|
||||
proxy.version = 5;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria') {
|
||||
// auth_str 将会在未来某个时候删除 但是有的机场不规范
|
||||
if (
|
||||
isPresent(proxy, 'auth_str') &&
|
||||
!isPresent(proxy, 'auth-str')
|
||||
) {
|
||||
proxy['auth-str'] = proxy['auth_str'];
|
||||
}
|
||||
if (isPresent(proxy, 'alpn')) {
|
||||
proxy.alpn = Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: [proxy.alpn];
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} else if (proxy.type === 'hysteria2') {
|
||||
if (
|
||||
isPresent(proxy, 'password') &&
|
||||
!isPresent(proxy, 'auth')
|
||||
) {
|
||||
proxy.auth = proxy.password;
|
||||
delete proxy.password;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'tfo') &&
|
||||
!isPresent(proxy, 'fast-open')
|
||||
) {
|
||||
proxy['fast-open'] = proxy.tfo;
|
||||
delete proxy.tfo;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'down') &&
|
||||
!isPresent(proxy, 'down-speed')
|
||||
) {
|
||||
proxy['down-speed'] = proxy.down;
|
||||
delete proxy.down;
|
||||
}
|
||||
if (
|
||||
isPresent(proxy, 'up') &&
|
||||
!isPresent(proxy, 'up-speed')
|
||||
) {
|
||||
proxy['up-speed'] = proxy.up;
|
||||
delete proxy.up;
|
||||
}
|
||||
if (isPresent(proxy, 'down-speed')) {
|
||||
proxy['down-speed'] =
|
||||
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
if (isPresent(proxy, 'up-speed')) {
|
||||
proxy['up-speed'] =
|
||||
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
|
||||
}
|
||||
} else if (proxy.type === 'wireguard') {
|
||||
proxy.keepalive =
|
||||
proxy.keepalive ?? proxy['persistent-keepalive'];
|
||||
proxy['persistent-keepalive'] = proxy.keepalive;
|
||||
proxy['preshared-key'] =
|
||||
proxy['preshared-key'] ?? proxy['pre-shared-key'];
|
||||
proxy['pre-shared-key'] = proxy['preshared-key'];
|
||||
} else if (proxy.type === '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.network === 'ws') {
|
||||
const wsPath = proxy['ws-opts']?.path;
|
||||
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, path = '', ed = ''] = reg.exec(wsPath);
|
||||
proxy['ws-opts'].path = path;
|
||||
if (ed !== '') {
|
||||
proxy['ws-opts']['early-data-header-name'] =
|
||||
'Sec-WebSocket-Protocol';
|
||||
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
|
||||
}
|
||||
}
|
||||
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 (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 };
|
||||
}
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
1134
backend/src/core/proxy-utils/producers/surge.js
Normal file
1134
backend/src/core/proxy-utils/producers/surge.js
Normal file
File diff suppressed because it is too large
Load Diff
192
backend/src/core/proxy-utils/producers/surgemac.js
Normal file
192
backend/src/core/proxy-utils/producers/surgemac.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import { Result, isPresent } from './utils';
|
||||
import Surge_Producer from './surge';
|
||||
import ClashMeta_Producer from './clashmeta';
|
||||
import { isIPv4, isIPv6 } from '@/utils';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const targetPlatform = 'SurgeMac';
|
||||
|
||||
const surge_Producer = Surge_Producer();
|
||||
|
||||
export default function SurgeMac_Producer() {
|
||||
const produce = (proxy, type, opts = {}) => {
|
||||
switch (proxy.type) {
|
||||
case 'external':
|
||||
return external(proxy);
|
||||
// case 'ssr':
|
||||
// return shadowsocksr(proxy);
|
||||
default: {
|
||||
try {
|
||||
return surge_Producer.produce(proxy, type, opts);
|
||||
} catch (e) {
|
||||
if (opts.useMihomoExternal) {
|
||||
$.log(
|
||||
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
|
||||
);
|
||||
return mihomo(proxy, type, opts);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return { produce };
|
||||
}
|
||||
function external(proxy) {
|
||||
const result = new Result(proxy);
|
||||
if (!proxy.exec || !proxy['local-port']) {
|
||||
throw new Error(`${proxy.type}: exec and local-port are required`);
|
||||
}
|
||||
result.append(
|
||||
`${proxy.name}=external,exec="${proxy.exec}",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({
|
||||
cipher: '-m',
|
||||
obfs: '-o',
|
||||
'obfs-param': '-g',
|
||||
password: '-k',
|
||||
port: '-p',
|
||||
protocol: '-O',
|
||||
'protocol-param': '-G',
|
||||
server: '-s',
|
||||
'local-port': '-l',
|
||||
'local-address': '-b',
|
||||
})) {
|
||||
if (external_proxy[key] != null) {
|
||||
external_proxy.args.push(value);
|
||||
external_proxy.args.push(external_proxy[key]);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
'default-nameserver': opts?.defaultNameserver ||
|
||||
proxy._defaultNameserver || [
|
||||
'180.76.76.76',
|
||||
'52.80.52.52',
|
||||
'119.28.28.28',
|
||||
'223.6.6.6',
|
||||
],
|
||||
nameserver: opts?.nameserver ||
|
||||
proxy._nameserver || [
|
||||
'https://doh.pub/dns-query',
|
||||
'https://dns.alidns.com/dns-query',
|
||||
'https://doh-pure.onedns.net/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);
|
||||
}
|
||||
682
backend/src/core/proxy-utils/producers/uri.js
Normal file
682
backend/src/core/proxy-utils/producers/uri.js
Normal file
@@ -0,0 +1,682 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
import { isIPv6 } from '@/utils';
|
||||
|
||||
export default function URI_Producer() {
|
||||
const type = 'SINGLE';
|
||||
const produce = (proxy) => {
|
||||
let result = '';
|
||||
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) {
|
||||
delete proxy[key];
|
||||
}
|
||||
}
|
||||
if (
|
||||
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
|
||||
proxy.type,
|
||||
)
|
||||
) {
|
||||
delete proxy.tls;
|
||||
}
|
||||
if (
|
||||
!['vmess'].includes(proxy.type) &&
|
||||
proxy.server &&
|
||||
isIPv6(proxy.server)
|
||||
) {
|
||||
proxy.server = `[${proxy.server}]`;
|
||||
}
|
||||
switch (proxy.type) {
|
||||
case 'socks5':
|
||||
result = `socks://${encodeURIComponent(
|
||||
Base64.encode(
|
||||
`${proxy.username ?? ''}:${proxy.password ?? ''}`,
|
||||
),
|
||||
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
|
||||
break;
|
||||
case 'ss':
|
||||
const userinfo = `${proxy.cipher}:${proxy.password}`;
|
||||
result = `ss://${
|
||||
proxy.cipher?.startsWith('2022-blake3-')
|
||||
? `${encodeURIComponent(
|
||||
proxy.cipher,
|
||||
)}:${encodeURIComponent(proxy.password)}`
|
||||
: Base64.encode(userinfo)
|
||||
}@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;
|
||||
if (proxy.plugin) {
|
||||
result += '?plugin=';
|
||||
const opts = proxy['plugin-opts'];
|
||||
switch (proxy.plugin) {
|
||||
case 'obfs':
|
||||
result += encodeURIComponent(
|
||||
`simple-obfs;obfs=${opts.mode}${
|
||||
opts.host ? ';obfs-host=' + opts.host : ''
|
||||
}`,
|
||||
);
|
||||
break;
|
||||
case 'v2ray-plugin':
|
||||
result += encodeURIComponent(
|
||||
`v2ray-plugin;obfs=${opts.mode}${
|
||||
opts.host ? ';obfs-host' + opts.host : ''
|
||||
}${opts.tls ? ';tls' : ''}`,
|
||||
);
|
||||
break;
|
||||
case 'shadow-tls':
|
||||
result += encodeURIComponent(
|
||||
`shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`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)}`;
|
||||
break;
|
||||
case 'ssr':
|
||||
result = `${proxy.server}:${proxy.port}:${proxy.protocol}:${
|
||||
proxy.cipher
|
||||
}:${proxy.obfs}:${Base64.encode(proxy.password)}/`;
|
||||
result += `?remarks=${Base64.encode(proxy.name)}${
|
||||
proxy['obfs-param']
|
||||
? '&obfsparam=' + Base64.encode(proxy['obfs-param'])
|
||||
: ''
|
||||
}${
|
||||
proxy['protocol-param']
|
||||
? '&protocolparam=' +
|
||||
Base64.encode(proxy['protocol-param'])
|
||||
: ''
|
||||
}`;
|
||||
result = 'ssr://' + Base64.encode(result);
|
||||
break;
|
||||
case 'vmess':
|
||||
// V2RayN URI format
|
||||
let type = '';
|
||||
let net = proxy.network || 'tcp';
|
||||
if (proxy.network === 'http') {
|
||||
net = 'tcp';
|
||||
type = 'http';
|
||||
} else if (
|
||||
proxy.network === 'ws' &&
|
||||
proxy['ws-opts']?.['v2ray-http-upgrade']
|
||||
) {
|
||||
net = 'httpupgrade';
|
||||
}
|
||||
result = {
|
||||
v: '2',
|
||||
ps: proxy.name,
|
||||
add: proxy.server,
|
||||
port: `${proxy.port}`,
|
||||
id: proxy.uuid,
|
||||
aid: `${proxy.alterId || 0}`,
|
||||
scy: proxy.cipher,
|
||||
net,
|
||||
type,
|
||||
tls: proxy.tls ? 'tls' : '',
|
||||
alpn: Array.isArray(proxy.alpn)
|
||||
? proxy.alpn.join(',')
|
||||
: proxy.alpn,
|
||||
fp: proxy['client-fingerprint'],
|
||||
};
|
||||
if (proxy.tls && proxy.sni) {
|
||||
result.sni = proxy.sni;
|
||||
}
|
||||
// obfs
|
||||
if (proxy.network) {
|
||||
let vmessTransportPath =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let vmessTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
|
||||
if (['grpc'].includes(proxy.network)) {
|
||||
result.path =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
'grpc-service-name'
|
||||
];
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
result.type =
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
|
||||
'gun';
|
||||
result.host =
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
||||
} else if (['kcp', 'quic'].includes(proxy.network)) {
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
result.type =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
`_${proxy.network}-type`
|
||||
] || 'none';
|
||||
result.host =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
`_${proxy.network}-host`
|
||||
];
|
||||
result.path =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
`_${proxy.network}-path`
|
||||
];
|
||||
} else {
|
||||
if (vmessTransportPath) {
|
||||
result.path = Array.isArray(vmessTransportPath)
|
||||
? vmessTransportPath[0]
|
||||
: vmessTransportPath;
|
||||
}
|
||||
if (vmessTransportHost) {
|
||||
result.host = Array.isArray(vmessTransportHost)
|
||||
? vmessTransportHost[0]
|
||||
: vmessTransportHost;
|
||||
}
|
||||
}
|
||||
}
|
||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||
break;
|
||||
case 'vless':
|
||||
let security = 'none';
|
||||
const isReality = proxy['reality-opts'];
|
||||
let sid = '';
|
||||
let pbk = '';
|
||||
let spx = '';
|
||||
if (isReality) {
|
||||
security = 'reality';
|
||||
const publicKey = proxy['reality-opts']?.['public-key'];
|
||||
if (publicKey) {
|
||||
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
|
||||
}
|
||||
const shortId = proxy['reality-opts']?.['short-id'];
|
||||
if (shortId) {
|
||||
sid = `&sid=${encodeURIComponent(shortId)}`;
|
||||
}
|
||||
const spiderX = proxy['reality-opts']?.['_spider-x'];
|
||||
if (spiderX) {
|
||||
spx = `&spx=${encodeURIComponent(spiderX)}`;
|
||||
}
|
||||
} else if (proxy.tls) {
|
||||
security = 'tls';
|
||||
}
|
||||
let alpn = '';
|
||||
if (proxy.alpn) {
|
||||
alpn = `&alpn=${encodeURIComponent(
|
||||
Array.isArray(proxy.alpn)
|
||||
? proxy.alpn
|
||||
: proxy.alpn.join(','),
|
||||
)}`;
|
||||
}
|
||||
let allowInsecure = '';
|
||||
if (proxy['skip-cert-verify']) {
|
||||
allowInsecure = `&allowInsecure=1`;
|
||||
}
|
||||
let sni = '';
|
||||
if (proxy.sni) {
|
||||
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
|
||||
}
|
||||
let fp = '';
|
||||
if (proxy['client-fingerprint']) {
|
||||
fp = `&fp=${encodeURIComponent(
|
||||
proxy['client-fingerprint'],
|
||||
)}`;
|
||||
}
|
||||
let flow = '';
|
||||
if (proxy.flow) {
|
||||
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
|
||||
}
|
||||
let extra = '';
|
||||
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)) {
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
vlessTransport += `&mode=${encodeURIComponent(
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
|
||||
)}`;
|
||||
const authority =
|
||||
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
||||
if (authority) {
|
||||
vlessTransport += `&authority=${encodeURIComponent(
|
||||
authority,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
let vlessTransportServiceName =
|
||||
proxy[`${proxy.network}-opts`]?.[
|
||||
`${proxy.network}-service-name`
|
||||
];
|
||||
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
|
||||
let vlessTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (vlessTransportPath) {
|
||||
vlessTransport += `&path=${encodeURIComponent(
|
||||
Array.isArray(vlessTransportPath)
|
||||
? vlessTransportPath[0]
|
||||
: vlessTransportPath,
|
||||
)}`;
|
||||
}
|
||||
if (vlessTransportHost) {
|
||||
vlessTransport += `&host=${encodeURIComponent(
|
||||
Array.isArray(vlessTransportHost)
|
||||
? vlessTransportHost[0]
|
||||
: vlessTransportHost,
|
||||
)}`;
|
||||
}
|
||||
if (vlessTransportServiceName) {
|
||||
vlessTransport += `&serviceName=${encodeURIComponent(
|
||||
vlessTransportServiceName,
|
||||
)}`;
|
||||
}
|
||||
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}:${
|
||||
proxy.port
|
||||
}?security=${encodeURIComponent(
|
||||
security,
|
||||
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
case 'trojan':
|
||||
let trojanTransport = '';
|
||||
if (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 =
|
||||
proxy[`${proxy.network}-opts`]?.path;
|
||||
let trojanTransportHost =
|
||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||
if (trojanTransportPath) {
|
||||
trojanTransport += `&path=${encodeURIComponent(
|
||||
Array.isArray(trojanTransportPath)
|
||||
? trojanTransportPath[0]
|
||||
: trojanTransportPath,
|
||||
)}`;
|
||||
}
|
||||
if (trojanTransportHost) {
|
||||
trojanTransport += `&host=${encodeURIComponent(
|
||||
Array.isArray(trojanTransportHost)
|
||||
? trojanTransportHost[0]
|
||||
: trojanTransportHost,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
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}:${
|
||||
proxy.port
|
||||
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
|
||||
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
|
||||
}${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
|
||||
proxy.name,
|
||||
)}`;
|
||||
break;
|
||||
case 'hysteria2':
|
||||
let hysteria2params = [];
|
||||
if (proxy['hop-interval']) {
|
||||
hysteria2params.push(
|
||||
`hop-interval=${proxy['hop-interval']}`,
|
||||
);
|
||||
}
|
||||
if (proxy['keepalive']) {
|
||||
hysteria2params.push(`keepalive=${proxy['keepalive']}`);
|
||||
}
|
||||
if (proxy['skip-cert-verify']) {
|
||||
hysteria2params.push(`insecure=1`);
|
||||
}
|
||||
if (proxy.obfs) {
|
||||
hysteria2params.push(
|
||||
`obfs=${encodeURIComponent(proxy.obfs)}`,
|
||||
);
|
||||
if (proxy['obfs-password']) {
|
||||
hysteria2params.push(
|
||||
`obfs-password=${encodeURIComponent(
|
||||
proxy['obfs-password'],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (proxy.sni) {
|
||||
hysteria2params.push(
|
||||
`sni=${encodeURIComponent(proxy.sni)}`,
|
||||
);
|
||||
}
|
||||
if (proxy.ports) {
|
||||
hysteria2params.push(`mport=${proxy.ports}`);
|
||||
}
|
||||
if (proxy['tls-fingerprint']) {
|
||||
hysteria2params.push(
|
||||
`pinSHA256=${encodeURIComponent(
|
||||
proxy['tls-fingerprint'],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
if (proxy.tfo) {
|
||||
hysteria2params.push(`fastopen=1`);
|
||||
}
|
||||
result = `hysteria2://${encodeURIComponent(proxy.password)}@${
|
||||
proxy.server
|
||||
}:${proxy.port}?${hysteria2params.join(
|
||||
'&',
|
||||
)}#${encodeURIComponent(proxy.name)}`;
|
||||
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] && !/^_/i.test(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',
|
||||
'tls',
|
||||
].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 (
|
||||
['congestion-controller'].includes(key)
|
||||
) {
|
||||
tuicParams.push(
|
||||
`congestion_control=${proxy[key]}`,
|
||||
);
|
||||
} else if (proxy[key] && !/^_/i.test(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 'anytls':
|
||||
let anytlsParams = [];
|
||||
Object.keys(proxy).forEach((key) => {
|
||||
if (
|
||||
![
|
||||
'name',
|
||||
'type',
|
||||
'password',
|
||||
'server',
|
||||
'port',
|
||||
'tls',
|
||||
].includes(key)
|
||||
) {
|
||||
const i = key.replace(/-/, '_');
|
||||
if (['alpn'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
anytlsParams.push(
|
||||
`${i}=${encodeURIComponent(
|
||||
Array.isArray(proxy[key])
|
||||
? proxy[key][0]
|
||||
: proxy[key],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else if (['skip-cert-verify'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
anytlsParams.push(`insecure=1`);
|
||||
}
|
||||
} else if (['udp'].includes(key)) {
|
||||
if (proxy[key]) {
|
||||
anytlsParams.push(`udp=1`);
|
||||
}
|
||||
} else if (proxy[key] && !/^_/i.test(key)) {
|
||||
anytlsParams.push(
|
||||
`${i.replace(/-/g, '_')}=${encodeURIComponent(
|
||||
proxy[key],
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result = `anytls://${encodeURIComponent(proxy.password)}@${
|
||||
proxy.server
|
||||
}:${proxy.port}/?${anytlsParams.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] && !/^_/i.test(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 { type, produce };
|
||||
}
|
||||
30
backend/src/core/proxy-utils/producers/utils.js
Normal file
30
backend/src/core/proxy-utils/producers/utils.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class Result {
|
||||
constructor(proxy) {
|
||||
this.proxy = proxy;
|
||||
this.output = [];
|
||||
}
|
||||
|
||||
append(data) {
|
||||
if (typeof data === 'undefined') {
|
||||
throw new Error('required field is missing');
|
||||
}
|
||||
this.output.push(data);
|
||||
}
|
||||
|
||||
appendIfPresent(data, attr) {
|
||||
if (isPresent(this.proxy, attr)) {
|
||||
this.append(data);
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.output.join('');
|
||||
}
|
||||
}
|
||||
|
||||
export function isPresent(obj, attr) {
|
||||
const data = _.get(obj, attr);
|
||||
return typeof data !== 'undefined' && data !== null;
|
||||
}
|
||||
30
backend/src/core/proxy-utils/producers/v2ray.js
Normal file
30
backend/src/core/proxy-utils/producers/v2ray.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { Base64 } from 'js-base64';
|
||||
import URI_Producer from './uri';
|
||||
import $ from '@/core/app';
|
||||
|
||||
const URI = URI_Producer();
|
||||
|
||||
export default function V2Ray_Producer() {
|
||||
const type = 'ALL';
|
||||
const produce = (proxies) => {
|
||||
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 };
|
||||
}
|
||||
0
backend/src/core/proxy-utils/validators/index.js
Normal file
0
backend/src/core/proxy-utils/validators/index.js
Normal file
69
backend/src/core/rule-utils/index.js
Normal file
69
backend/src/core/rule-utils/index.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import RULE_PREPROCESSORS from './preprocessors';
|
||||
import RULE_PRODUCERS from './producers';
|
||||
import RULE_PARSERS from './parsers';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export const RuleUtils = (function () {
|
||||
function preprocess(raw) {
|
||||
for (const processor of RULE_PREPROCESSORS) {
|
||||
try {
|
||||
if (processor.test(raw)) {
|
||||
$.info(`Pre-processor [${processor.name}] activated`);
|
||||
return processor.parse(raw);
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parse(raw) {
|
||||
raw = preprocess(raw);
|
||||
for (const parser of RULE_PARSERS) {
|
||||
let matched;
|
||||
try {
|
||||
matched = parser.test(raw);
|
||||
} catch (err) {
|
||||
matched = false;
|
||||
}
|
||||
if (matched) {
|
||||
$.info(`Rule parser [${parser.name}] is activated!`);
|
||||
return parser.parse(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function produce(rules, targetPlatform) {
|
||||
const producer = RULE_PRODUCERS[targetPlatform];
|
||||
if (!producer) {
|
||||
throw new Error(
|
||||
`Target platform: ${targetPlatform} is not supported!`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof producer.type === 'undefined' ||
|
||||
producer.type === 'SINGLE'
|
||||
) {
|
||||
return rules
|
||||
.map((rule) => {
|
||||
try {
|
||||
return producer.func(rule);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
`ERROR: cannot produce rule: ${JSON.stringify(
|
||||
rule,
|
||||
)}\nReason: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.filter((line) => line.length > 0)
|
||||
.join('\n');
|
||||
} else if (producer.type === 'ALL') {
|
||||
return producer.func(rules);
|
||||
}
|
||||
}
|
||||
|
||||
return { parse, produce };
|
||||
})();
|
||||
59
backend/src/core/rule-utils/parsers.js
Normal file
59
backend/src/core/rule-utils/parsers.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const RULE_TYPES_MAPPING = [
|
||||
[/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
|
||||
[/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],
|
||||
[/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],
|
||||
[/^USER-AGENT$/i, 'USER-AGENT'],
|
||||
[/^PROCESS-NAME$/, 'PROCESS-NAME'],
|
||||
[/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],
|
||||
[/^SRC-IP(-CIDR)?$/, 'SRC-IP'],
|
||||
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
||||
[/^PROTOCOL$/, 'PROTOCOL'],
|
||||
[/^IP-CIDR$/i, 'IP-CIDR'],
|
||||
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
|
||||
[/^GEOIP$/i, 'GEOIP'],
|
||||
[/^GEOSITE$/i, 'GEOSITE'],
|
||||
];
|
||||
|
||||
function AllRuleParser() {
|
||||
const name = 'Universal Rule Parser';
|
||||
const test = () => true;
|
||||
const parse = (raw) => {
|
||||
const lines = raw.split('\n');
|
||||
const result = [];
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
// skip empty line
|
||||
if (line.length === 0) continue;
|
||||
// skip comments
|
||||
if (/\s*#/.test(line)) continue;
|
||||
try {
|
||||
const params = line.split(',').map((w) => w.trim());
|
||||
let rawType = params[0];
|
||||
let matched = false;
|
||||
for (const item of RULE_TYPES_MAPPING) {
|
||||
const regex = item[0];
|
||||
if (regex.test(rawType)) {
|
||||
matched = true;
|
||||
const rule = {
|
||||
type: item[1],
|
||||
content: params[1],
|
||||
};
|
||||
if (
|
||||
['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)
|
||||
) {
|
||||
rule.options = params.slice(2);
|
||||
}
|
||||
result.push(rule);
|
||||
}
|
||||
}
|
||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
||||
} catch (e) {
|
||||
console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [AllRuleParser()];
|
||||
18
backend/src/core/rule-utils/preprocessors.js
Normal file
18
backend/src/core/rule-utils/preprocessors.js
Normal file
@@ -0,0 +1,18 @@
|
||||
function HTML() {
|
||||
const name = 'HTML';
|
||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||
// simply discard HTML
|
||||
const parse = () => '';
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
function ClashProvider() {
|
||||
const name = 'Clash Provider';
|
||||
const test = (raw) => /^payload:/gm.exec(raw).index >= 0;
|
||||
const parse = (raw) => {
|
||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||
};
|
||||
return { name, test, parse };
|
||||
}
|
||||
|
||||
export default [HTML(), ClashProvider()];
|
||||
101
backend/src/core/rule-utils/producers.js
Normal file
101
backend/src/core/rule-utils/producers.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import YAML from '@/utils/yaml';
|
||||
|
||||
function QXFilter() {
|
||||
const type = 'SINGLE';
|
||||
const func = (rule) => {
|
||||
// skip unsupported rules
|
||||
const UNSUPPORTED = [
|
||||
'URL-REGEX',
|
||||
'DEST-PORT',
|
||||
'SRC-IP',
|
||||
'IN-PORT',
|
||||
'PROTOCOL',
|
||||
'GEOSITE',
|
||||
'GEOIP',
|
||||
];
|
||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||
|
||||
const TRANSFORM = {
|
||||
'DOMAIN-KEYWORD': 'HOST-KEYWORD',
|
||||
'DOMAIN-SUFFIX': 'HOST-SUFFIX',
|
||||
DOMAIN: 'HOST',
|
||||
'IP-CIDR6': 'IP6-CIDR',
|
||||
};
|
||||
|
||||
// QX does not support the no-resolve option
|
||||
return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`;
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
function SurgeRuleSet() {
|
||||
const type = 'SINGLE';
|
||||
const func = (rule) => {
|
||||
const UNSUPPORTED = ['GEOSITE', 'GEOIP'];
|
||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||
let output = `${rule.type},${rule.content}`;
|
||||
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
|
||||
output +=
|
||||
rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';
|
||||
}
|
||||
return output;
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
function LoonRules() {
|
||||
const type = 'SINGLE';
|
||||
const func = (rule) => {
|
||||
// skip unsupported rules
|
||||
const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];
|
||||
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 { type, func };
|
||||
}
|
||||
|
||||
function ClashRuleProvider() {
|
||||
const type = 'ALL';
|
||||
const func = (rules) => {
|
||||
const TRANSFORM = {
|
||||
'DEST-PORT': 'DST-PORT',
|
||||
'SRC-IP': 'SRC-IP-CIDR',
|
||||
'IN-PORT': 'SRC-PORT',
|
||||
};
|
||||
const conf = {
|
||||
payload: rules.map((rule) => {
|
||||
let output = `${TRANSFORM[rule.type] || rule.type},${
|
||||
rule.content
|
||||
}`;
|
||||
if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
|
||||
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 YAML.dump(conf);
|
||||
};
|
||||
return { type, func };
|
||||
}
|
||||
|
||||
export default {
|
||||
QX: QXFilter(),
|
||||
Surge: SurgeRuleSet(),
|
||||
Loon: LoonRules(),
|
||||
Clash: ClashRuleProvider(),
|
||||
};
|
||||
26
backend/src/main.js
Normal file
26
backend/src/main.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* ███████╗██╗ ██╗██████╗ ███████╗████████╗ ██████╗ ██████╗ ███████╗
|
||||
* ██╔════╝██║ ██║██╔══██╗ ██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝
|
||||
* ███████╗██║ ██║██████╔╝█████╗███████╗ ██║ ██║ ██║██████╔╝█████╗
|
||||
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
||||
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
||||
* @author: Peng-YM
|
||||
* @github: https://github.com/sub-store-org/Sub-Store
|
||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||
*/
|
||||
import { version } from '../package.json';
|
||||
import $ from '@/core/app';
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
import migrate from '@/utils/migration';
|
||||
import serve from '@/restful';
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
250
backend/src/products/cron-sync-artifacts.js
Normal file
250
backend/src/products/cron-sync-artifacts.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import { version } from '../../package.json';
|
||||
import {
|
||||
SETTINGS_KEY,
|
||||
ARTIFACTS_KEY,
|
||||
SUBS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
} from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { syncToGist } from '@/restful/artifacts';
|
||||
import { findByName } from '@/utils/database';
|
||||
|
||||
!(async function () {
|
||||
let arg;
|
||||
if (typeof $argument != 'undefined') {
|
||||
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);
|
||||
if (!artifacts || artifacts.length === 0) return;
|
||||
|
||||
const shouldSync = artifacts.some((artifact) => artifact.sync);
|
||||
if (shouldSync) await doSync();
|
||||
}
|
||||
})().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() {
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store Sync -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const files = {};
|
||||
|
||||
try {
|
||||
const valid = [];
|
||||
const invalid = [];
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const subNames = [];
|
||||
let enabledCount = 0;
|
||||
allArtifacts.map((artifact) => {
|
||||
if (artifact.sync && artifact.source) {
|
||||
enabledCount++;
|
||||
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 (enabledCount === 0) {
|
||||
$.info(
|
||||
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
allArtifacts.map(async (artifact) => {
|
||||
try {
|
||||
if (artifact.sync && artifact.source) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
|
||||
const useMihomoExternal =
|
||||
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 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) {
|
||||
if (
|
||||
artifact.sync &&
|
||||
artifact.source &&
|
||||
valid.includes(artifact.name)
|
||||
) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
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);
|
||||
$.info('上传配置成功');
|
||||
|
||||
if (invalid.length > 0) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
|
||||
);
|
||||
} else {
|
||||
$.notify('🌍 Sub-Store', '同步配置完成');
|
||||
}
|
||||
} catch (e) {
|
||||
$.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`);
|
||||
$.error(`无法同步配置到 Gist,原因:${e}`);
|
||||
}
|
||||
}
|
||||
103
backend/src/products/resource-parser.loon.js
Normal file
103
backend/src/products/resource-parser.loon.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable no-undef */
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { RuleUtils } from '@/core/rule-utils';
|
||||
import { version } from '../../package.json';
|
||||
import download from '@/utils/download';
|
||||
|
||||
let result = '';
|
||||
let resource = typeof $resource !== 'undefined' ? $resource : '';
|
||||
let resourceType = typeof $resourceType !== 'undefined' ? $resourceType : '';
|
||||
let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
|
||||
|
||||
!(async () => {
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
Loon -- ${$loon}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
const build = $loon.match(/\((\d+)\)$/)?.[1];
|
||||
let arg;
|
||||
if (typeof $argument != 'undefined') {
|
||||
arg = Object.fromEntries(
|
||||
$argument.split('&').map((item) => item.split('=')),
|
||||
);
|
||||
} else {
|
||||
arg = {};
|
||||
}
|
||||
console.log(`arg: ${JSON.stringify(arg)}`);
|
||||
|
||||
const RESOURCE_TYPE = {
|
||||
PROXY: 1,
|
||||
RULE: 2,
|
||||
};
|
||||
if (!arg.resourceUrlOnly) {
|
||||
result = resource;
|
||||
}
|
||||
|
||||
if (resourceType === RESOURCE_TYPE.PROXY) {
|
||||
if (!arg.resourceUrlOnly) {
|
||||
try {
|
||||
let proxies = ProxyUtils.parse(resource);
|
||||
result = ProxyUtils.produce(proxies, 'Loon', undefined, {
|
||||
'include-unsupported-proxy':
|
||||
arg?.includeUnsupportedProxy || build >= 842,
|
||||
});
|
||||
} 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 || build >= 842,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e.message ?? e);
|
||||
}
|
||||
}
|
||||
} else if (resourceType === RESOURCE_TYPE.RULE) {
|
||||
if (!arg.resourceUrlOnly) {
|
||||
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 || '');
|
||||
});
|
||||
45
backend/src/products/sub-store-0.js
Normal file
45
backend/src/products/sub-store-0.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 路由拆分 - 本文件只包含不涉及到解析器的 RESTFul API
|
||||
*/
|
||||
|
||||
import { version } from '../../package.json';
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
import migrate from '@/utils/migration';
|
||||
import express from '@/vendor/express';
|
||||
import $ from '@/core/app';
|
||||
import registerCollectionRoutes from '@/restful/collections';
|
||||
import registerSubscriptionRoutes from '@/restful/subscriptions';
|
||||
import registerArtifactRoutes from '@/restful/artifacts';
|
||||
import registerSettingRoutes from '@/restful/settings';
|
||||
import registerMiscRoutes from '@/restful/miscs';
|
||||
import registerSortRoutes from '@/restful/sort';
|
||||
import registerFileRoutes from '@/restful/file';
|
||||
import registerTokenRoutes from '@/restful/token';
|
||||
import registerModuleRoutes from '@/restful/module';
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
|
||||
function serve() {
|
||||
const $app = express({ substore: $ });
|
||||
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerTokenRoutes($app);
|
||||
registerFileRoutes($app);
|
||||
registerModuleRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerSortRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
|
||||
$app.start();
|
||||
}
|
||||
39
backend/src/products/sub-store-1.js
Normal file
39
backend/src/products/sub-store-1.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 路由拆分 - 本文件仅包含使用到解析器的 RESTFul API
|
||||
*/
|
||||
|
||||
import { version } from '../../package.json';
|
||||
import migrate from '@/utils/migration';
|
||||
import express from '@/vendor/express';
|
||||
import $ from '@/core/app';
|
||||
import registerDownloadRoutes from '@/restful/download';
|
||||
import registerPreviewRoutes from '@/restful/preview';
|
||||
import registerSyncRoutes from '@/restful/sync';
|
||||
import registerNodeInfoRoutes from '@/restful/node-info';
|
||||
|
||||
console.log(
|
||||
`
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
Sub-Store -- v${version}
|
||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||
`,
|
||||
);
|
||||
|
||||
migrate();
|
||||
serve();
|
||||
|
||||
function serve() {
|
||||
const $app = express({ substore: $ });
|
||||
|
||||
// register routes
|
||||
registerDownloadRoutes($app);
|
||||
registerPreviewRoutes($app);
|
||||
registerSyncRoutes($app);
|
||||
registerNodeInfoRoutes($app);
|
||||
|
||||
$app.options('/', (req, res) => {
|
||||
res.status(200).end();
|
||||
});
|
||||
|
||||
$app.start();
|
||||
}
|
||||
274
backend/src/restful/artifacts.js
Normal file
274
backend/src/restful/artifacts.js
Normal file
@@ -0,0 +1,274 @@
|
||||
import $ from '@/core/app';
|
||||
|
||||
import {
|
||||
ARTIFACT_REPOSITORY_KEY,
|
||||
ARTIFACTS_KEY,
|
||||
SETTINGS_KEY,
|
||||
} from '@/constants';
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import {
|
||||
InternalServerError,
|
||||
RequestInvalidError,
|
||||
ResourceNotFoundError,
|
||||
} from '@/restful/errors';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
export default function register($app) {
|
||||
// Initialization
|
||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||
|
||||
// RESTful APIs
|
||||
$app.get('/api/artifacts/restore', restoreArtifacts);
|
||||
|
||||
$app.route('/api/artifacts')
|
||||
.get(getAllArtifacts)
|
||||
.post(createArtifact)
|
||||
.put(replaceArtifact);
|
||||
|
||||
$app.route('/api/artifact/:name')
|
||||
.get(getArtifact)
|
||||
.patch(updateArtifact)
|
||||
.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) {
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
success(res, allArtifacts);
|
||||
}
|
||||
|
||||
function replaceArtifact(req, res) {
|
||||
const allArtifacts = req.body;
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
async function getArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const artifact = findByName(allArtifacts, name);
|
||||
|
||||
if (artifact) {
|
||||
success(res, artifact);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Artifact ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createArtifact(req, res) {
|
||||
const artifact = req.body;
|
||||
if (!validateArtifactName(artifact.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_ARTIFACT_NAME',
|
||||
`Artifact name ${artifact.name} is invalid.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$.info(`正在创建远程配置:${artifact.name}`);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
if (findByName(allArtifacts, artifact.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
`Artifact ${artifact.name} already exists.`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
allArtifacts.push(artifact);
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, artifact, 201);
|
||||
}
|
||||
}
|
||||
|
||||
function updateArtifact(req, res) {
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
let oldName = req.params.name;
|
||||
const artifact = findByName(allArtifacts, oldName);
|
||||
if (artifact) {
|
||||
$.info(`正在更新远程配置:${artifact.name}`);
|
||||
const newArtifact = {
|
||||
...artifact,
|
||||
...req.body,
|
||||
};
|
||||
if (!validateArtifactName(newArtifact.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_ARTIFACT_NAME',
|
||||
`Artifact name ${newArtifact.name} is invalid.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
updateByName(allArtifacts, oldName, newArtifact);
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res, newArtifact);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
`Artifact ${oldName} already exists.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
$.info(`正在删除远程配置:${name}`);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
try {
|
||||
const artifact = findByName(allArtifacts, name);
|
||||
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
|
||||
if (artifact.updated) {
|
||||
// delete gist
|
||||
const files = {};
|
||||
files[encodeURIComponent(artifact.name)] = {
|
||||
content: '',
|
||||
};
|
||||
if (encodeURIComponent(artifact.name) !== artifact.name) {
|
||||
files[artifact.name] = {
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
|
||||
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
|
||||
try {
|
||||
await syncToGist(files);
|
||||
} catch (i) {
|
||||
$.error(`Function syncToGist: ${name} : ${i}`);
|
||||
}
|
||||
}
|
||||
// delete local cache
|
||||
deleteByName(allArtifacts, name);
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
success(res);
|
||||
} catch (err) {
|
||||
$.error(`无法删除远程配置:${name},原因:${err}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_DELETE_ARTIFACT`,
|
||||
`Failed to delete artifact ${name}`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateArtifactName(name) {
|
||||
return /^[a-zA-Z0-9._-]*$/.test(name);
|
||||
}
|
||||
|
||||
async function syncToGist(files) {
|
||||
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
return Promise.reject('未设置 GitHub Token!');
|
||||
}
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
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 };
|
||||
154
backend/src/restful/collections.js
Normal file
154
backend/src/restful/collections.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
|
||||
|
||||
$app.route('/api/collection/:name')
|
||||
.get(getCollection)
|
||||
.patch(updateCollection)
|
||||
.delete(deleteCollection);
|
||||
|
||||
$app.route('/api/collections')
|
||||
.get(getAllCollections)
|
||||
.post(createCollection)
|
||||
.put(replaceCollection);
|
||||
}
|
||||
|
||||
// collection API
|
||||
function createCollection(req, res) {
|
||||
const collection = req.body;
|
||||
$.info(`正在创建组合订阅:${collection.name}`);
|
||||
if (/\//.test(collection.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_NAME',
|
||||
`Collection ${collection.name} is invalid`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
if (findByName(allCols, collection.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
`Collection ${collection.name} already exists.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
allCols.push(collection);
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res, collection, 201);
|
||||
}
|
||||
|
||||
function getCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
let { raw } = req.query;
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
if (collection) {
|
||||
if (raw) {
|
||||
res.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_collection_${name}_${formatDateTime(
|
||||
new Date(),
|
||||
)}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(JSON.stringify(collection));
|
||||
} else {
|
||||
success(res, collection);
|
||||
}
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
`SUBSCRIPTION_NOT_FOUND`,
|
||||
`Collection ${name} does not exist`,
|
||||
404,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
let collection = req.body;
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const oldCol = findByName(allCols, name);
|
||||
if (oldCol) {
|
||||
const newCol = {
|
||||
...oldCol,
|
||||
...collection,
|
||||
};
|
||||
$.info(`正在更新组合订阅:${name}...`);
|
||||
|
||||
if (name !== newCol.name) {
|
||||
// update all artifacts referring this collection
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
|
||||
for (const artifact of allArtifacts) {
|
||||
if (
|
||||
artifact.type === 'collection' &&
|
||||
artifact.source === oldCol.name
|
||||
) {
|
||||
artifact.source = newCol.name;
|
||||
}
|
||||
}
|
||||
// update all files referring this collection
|
||||
const allFiles = $.read(FILES_KEY) || [];
|
||||
for (const file of allFiles) {
|
||||
if (
|
||||
file.sourceType === 'collection' &&
|
||||
file.sourceName === oldCol.name
|
||||
) {
|
||||
file.sourceName = newCol.name;
|
||||
}
|
||||
}
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
}
|
||||
|
||||
updateByName(allCols, name, newCol);
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res, newCol);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Collection ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCollection(req, res) {
|
||||
let { name } = req.params;
|
||||
$.info(`正在删除组合订阅:${name}`);
|
||||
let allCols = $.read(COLLECTIONS_KEY);
|
||||
deleteByName(allCols, name);
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
function getAllCollections(req, res) {
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
success(res, allCols);
|
||||
}
|
||||
|
||||
function replaceCollection(req, res) {
|
||||
const allCols = req.body;
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res);
|
||||
}
|
||||
762
backend/src/restful/download.js
Normal file
762
backend/src/restful/download.js
Normal file
@@ -0,0 +1,762 @@
|
||||
import {
|
||||
getPlatformFromHeaders,
|
||||
shouldIncludeUnsupportedProxy,
|
||||
} from '@/utils/user-agent';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||||
import $ from '@/core/app';
|
||||
import { failed } from '@/restful/response';
|
||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||
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) {
|
||||
$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/: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/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) {
|
||||
let { name, nezhaIndex } = req.params;
|
||||
|
||||
const useMihomoExternal = req.query.target === 'SurgeMac';
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
|
||||
$.info(
|
||||
`正在下载订阅:${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 = {
|
||||
_req: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
},
|
||||
};
|
||||
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) {
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
if (!/^https?:\/\//.test(url)) {
|
||||
content = url;
|
||||
$.info(`URL 不是链接,视为本地订阅`);
|
||||
}
|
||||
}
|
||||
if (content) {
|
||||
$.info(`指定本地订阅: ${content}`);
|
||||
}
|
||||
if (proxy) {
|
||||
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||||
}
|
||||
if (ua) {
|
||||
$.info(`指定远程订阅 User-Agent: ${ua}`);
|
||||
}
|
||||
|
||||
if (mergeSources) {
|
||||
$.info(`指定合并来源: ${mergeSources}`);
|
||||
}
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||
}
|
||||
if (produceType) {
|
||||
$.info(`指定生产类型: ${produceType}`);
|
||||
}
|
||||
if (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 sub = findByName(allSubs, name);
|
||||
if (sub) {
|
||||
try {
|
||||
const passThroughUA = sub.passThroughUA;
|
||||
if (passThroughUA) {
|
||||
$.info(
|
||||
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
|
||||
);
|
||||
ua = reqUA;
|
||||
}
|
||||
let output = await produceArtifact({
|
||||
type: 'subscription',
|
||||
name,
|
||||
platform,
|
||||
url,
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteSub,
|
||||
produceType,
|
||||
produceOpts: {
|
||||
'include-unsupported-proxy': includeUnsupportedProxy,
|
||||
useMihomoExternal,
|
||||
},
|
||||
$options,
|
||||
proxy,
|
||||
noCache,
|
||||
});
|
||||
let flowInfo;
|
||||
if (
|
||||
sub.source !== 'local' ||
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
try {
|
||||
url =
|
||||
`${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 && /^https?/.test(url)) {
|
||||
// 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) {
|
||||
$.error(
|
||||
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
|
||||
err,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 (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(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.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 subscription: ${name}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}!`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Subscription ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadCollection(req, res) {
|
||||
let { name, nezhaIndex } = req.params;
|
||||
|
||||
const useMihomoExternal = req.query.target === 'SurgeMac';
|
||||
|
||||
const platform =
|
||||
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
|
||||
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
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}`,
|
||||
);
|
||||
|
||||
let {
|
||||
ignoreFailedRemoteSub,
|
||||
produceType,
|
||||
includeUnsupportedProxy,
|
||||
resultFormat,
|
||||
proxy,
|
||||
noCache,
|
||||
} = req.query;
|
||||
|
||||
let $options = {
|
||||
_req: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
},
|
||||
};
|
||||
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) {
|
||||
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||||
}
|
||||
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
|
||||
}
|
||||
if (produceType) {
|
||||
$.info(`指定生产类型: ${produceType}`);
|
||||
}
|
||||
|
||||
if (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) {
|
||||
try {
|
||||
let output = await produceArtifact({
|
||||
type: 'collection',
|
||||
name,
|
||||
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
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const subnames = collection.subscriptions;
|
||||
if (subnames.length > 0) {
|
||||
const sub = findByName(allSubs, subnames[0]);
|
||||
if (
|
||||
sub.source !== 'local' ||
|
||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
try {
|
||||
let 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 && /^https?:/.test(url)) {
|
||||
subUserInfoOfSub = await getFlowHeaders(
|
||||
$arguments?.insecure ? `${url}#insecure` : url,
|
||||
$arguments.flowUserAgent,
|
||||
undefined,
|
||||
proxy || sub.proxy || collection.proxy,
|
||||
$arguments.flowUrl,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`组合订阅 ${name} 中的子订阅 ${
|
||||
sub.name
|
||||
} 获取流量信息时发生错误: ${err.message ?? err}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 (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(
|
||||
output,
|
||||
);
|
||||
} else {
|
||||
res.send(output);
|
||||
}
|
||||
} catch (err) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 下载组合订阅失败`,
|
||||
`❌ 下载组合订阅错误:${name}!`,
|
||||
`🤔 原因:${err}`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'INTERNAL_SERVER_ERROR',
|
||||
`Failed to download collection: ${name}`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$.error(
|
||||
`🌍 Sub-Store 下载组合订阅失败`,
|
||||
`❌ 未找到组合订阅:${name}!`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Collection ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
35
backend/src/restful/errors/index.js
Normal file
35
backend/src/restful/errors/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
class BaseError {
|
||||
constructor(code, message, details) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalServerError extends BaseError {
|
||||
constructor(code, message, details) {
|
||||
super(code, message, details);
|
||||
this.type = 'InternalServerError';
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestInvalidError extends BaseError {
|
||||
constructor(code, message, details) {
|
||||
super(code, message, details);
|
||||
this.type = 'RequestInvalidError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceNotFoundError extends BaseError {
|
||||
constructor(code, message, details) {
|
||||
super(code, message, details);
|
||||
this.type = 'ResourceNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkError extends BaseError {
|
||||
constructor(code, message, details) {
|
||||
super(code, message, details);
|
||||
this.type = 'NetworkError';
|
||||
}
|
||||
}
|
||||
314
backend/src/restful/file.js
Normal file
314
backend/src/restful/file.js
Normal file
@@ -0,0 +1,314 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
|
||||
import { FILES_KEY, ARTIFACTS_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
RequestInvalidError,
|
||||
ResourceNotFoundError,
|
||||
InternalServerError,
|
||||
} from '@/restful/errors';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
|
||||
|
||||
$app.get('/share/file/:name', getFile);
|
||||
|
||||
$app.route('/api/file/:name')
|
||||
.get(getFile)
|
||||
.patch(updateFile)
|
||||
.delete(deleteFile);
|
||||
|
||||
$app.route('/api/wholeFile/:name').get(getWholeFile);
|
||||
|
||||
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
|
||||
$app.route('/api/wholeFiles').get(getAllWholeFiles);
|
||||
}
|
||||
|
||||
// file API
|
||||
function createFile(req, res) {
|
||||
const file = req.body;
|
||||
file.name = `${file.name ?? Date.now()}`;
|
||||
$.info(`正在创建文件:${file.name}`);
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
if (findByName(allFiles, file.name)) {
|
||||
return failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
req.body.name
|
||||
? `已存在 name 为 ${file.name} 的文件`
|
||||
: `无法同时创建相同的文件 可稍后重试`,
|
||||
),
|
||||
);
|
||||
}
|
||||
allFiles.push(file);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res, file, 201);
|
||||
}
|
||||
|
||||
async function getFile(req, res) {
|
||||
let { name } = req.params;
|
||||
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
|
||||
$.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`);
|
||||
let {
|
||||
url,
|
||||
subInfoUrl,
|
||||
subInfoUserAgent,
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteFile,
|
||||
proxy,
|
||||
noCache,
|
||||
produceType,
|
||||
} = req.query;
|
||||
let $options = {
|
||||
_req: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
path: req.path,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
},
|
||||
};
|
||||
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) {
|
||||
$.info(`指定远程文件 URL: ${url}`);
|
||||
}
|
||||
if (proxy) {
|
||||
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
|
||||
}
|
||||
if (ua) {
|
||||
$.info(`指定远程文件 User-Agent: ${ua}`);
|
||||
}
|
||||
if (subInfoUrl) {
|
||||
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
|
||||
}
|
||||
if (subInfoUserAgent) {
|
||||
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
|
||||
}
|
||||
if (content) {
|
||||
$.info(`指定本地文件: ${content}`);
|
||||
}
|
||||
if (mergeSources) {
|
||||
$.info(`指定合并来源: ${mergeSources}`);
|
||||
}
|
||||
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
|
||||
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
|
||||
}
|
||||
if (noCache) {
|
||||
$.info(`指定不使用缓存: ${noCache}`);
|
||||
}
|
||||
if (produceType) {
|
||||
$.info(`指定生产类型: ${produceType}`);
|
||||
}
|
||||
|
||||
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,
|
||||
produceType,
|
||||
all: true,
|
||||
});
|
||||
|
||||
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');
|
||||
if (output?.$options?._res?.headers) {
|
||||
Object.entries(output.$options._res.headers).forEach(
|
||||
([key, value]) => {
|
||||
res.set(key, value);
|
||||
},
|
||||
);
|
||||
}
|
||||
res.send(output?.$content ?? '');
|
||||
} 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;
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (file) {
|
||||
if (raw) {
|
||||
res.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_file_${name}_${formatDateTime(
|
||||
new Date(),
|
||||
)}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(JSON.stringify(file));
|
||||
} else {
|
||||
success(res, file);
|
||||
}
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
`FILE_NOT_FOUND`,
|
||||
`File ${name} does not exist`,
|
||||
404,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFile(req, res) {
|
||||
let { name } = req.params;
|
||||
let file = req.body;
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const oldFile = findByName(allFiles, name);
|
||||
if (oldFile) {
|
||||
const newFile = {
|
||||
...oldFile,
|
||||
...file,
|
||||
};
|
||||
$.info(`正在更新文件:${name}...`);
|
||||
|
||||
if (name !== newFile.name) {
|
||||
// update all artifacts referring this collection
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
|
||||
for (const artifact of allArtifacts) {
|
||||
if (
|
||||
artifact.type === 'file' &&
|
||||
artifact.source === oldFile.name
|
||||
) {
|
||||
artifact.source = newFile.name;
|
||||
}
|
||||
}
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
}
|
||||
|
||||
updateByName(allFiles, name, newFile);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res, newFile);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`File ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteFile(req, res) {
|
||||
let { name } = req.params;
|
||||
$.info(`正在删除文件:${name}`);
|
||||
let allFiles = $.read(FILES_KEY);
|
||||
deleteByName(allFiles, name);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
function getAllFiles(req, res) {
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
success(
|
||||
res, // eslint-disable-next-line no-unused-vars
|
||||
allFiles.map(({ content, ...rest }) => rest),
|
||||
);
|
||||
}
|
||||
|
||||
function getAllWholeFiles(req, res) {
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
success(res, allFiles);
|
||||
}
|
||||
|
||||
function replaceFile(req, res) {
|
||||
const allFiles = req.body;
|
||||
$.write(allFiles, FILES_KEY);
|
||||
success(res);
|
||||
}
|
||||
475
backend/src/restful/index.js
Normal file
475
backend/src/restful/index.js
Normal file
@@ -0,0 +1,475 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import _ from 'lodash';
|
||||
import express from '@/vendor/express';
|
||||
import $ from '@/core/app';
|
||||
import migrate from '@/utils/migration';
|
||||
import download, { downloadFile } from '@/utils/download';
|
||||
import { syncArtifacts, produceArtifact } from '@/restful/sync';
|
||||
import { gistBackupAction } from '@/restful/miscs';
|
||||
import { TOKENS_KEY } from '@/constants';
|
||||
|
||||
import registerSubscriptionRoutes from './subscriptions';
|
||||
import registerCollectionRoutes from './collections';
|
||||
import registerArtifactRoutes from './artifacts';
|
||||
import registerFileRoutes from './file';
|
||||
import registerTokenRoutes from './token';
|
||||
import registerModuleRoutes from './module';
|
||||
import registerSyncRoutes from './sync';
|
||||
import registerDownloadRoutes from './download';
|
||||
import registerSettingRoutes from './settings';
|
||||
import registerPreviewRoutes from './preview';
|
||||
import registerSortingRoutes from './sort';
|
||||
import registerMiscRoutes from './miscs';
|
||||
import registerNodeInfoRoutes from './node-info';
|
||||
import registerParserRoutes from './parser';
|
||||
|
||||
export default function serve() {
|
||||
let port;
|
||||
let host;
|
||||
if ($.env.isNode) {
|
||||
port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;
|
||||
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
|
||||
}
|
||||
const $app = express({ substore: $, port, host });
|
||||
if ($.env.isNode) {
|
||||
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
|
||||
const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');
|
||||
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
|
||||
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
|
||||
if (be_prefix || be_merge) {
|
||||
if (!fe_be_path.startsWith('/')) {
|
||||
throw new Error(
|
||||
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
|
||||
);
|
||||
}
|
||||
if (be_merge) {
|
||||
$.info(`[BACKEND] MERGE mode is [ON].`);
|
||||
$.info(`[BACKEND && FRONTEND] ${host}:${port}`);
|
||||
}
|
||||
$.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);
|
||||
$app.use((req, res, next) => {
|
||||
if (req.path.startsWith(fe_be_path)) {
|
||||
req.url = req.url.replace(fe_be_path, '') || '/';
|
||||
if (be_merge && req.url.startsWith('/api/')) {
|
||||
req.query['share'] = 'true';
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const pathname =
|
||||
decodeURIComponent(req._parsedUrl.pathname) || '/';
|
||||
if (
|
||||
be_merge &&
|
||||
req.path.startsWith('/share/') &&
|
||||
req.query.token
|
||||
) {
|
||||
if (req.method.toLowerCase() !== 'get') {
|
||||
res.status(405).send('Method not allowed');
|
||||
return;
|
||||
}
|
||||
const tokens = $.read(TOKENS_KEY) || [];
|
||||
const token = tokens.find(
|
||||
(t) =>
|
||||
t.token === req.query.token &&
|
||||
`/share/${t.type}/${t.name}` === pathname &&
|
||||
(t.exp == null || t.exp > Date.now()),
|
||||
);
|
||||
if (token) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (be_merge && fe_path && req.path.indexOf('/', 1) == -1) {
|
||||
if (req.path.indexOf('.') == -1) {
|
||||
req.url = '/index.html';
|
||||
}
|
||||
const express_ = eval(`require("express")`);
|
||||
const mime_ = eval(`require("mime-types")`);
|
||||
const path_ = eval(`require("path")`);
|
||||
const staticFileMiddleware = express_.static(fe_path, {
|
||||
setHeaders: (res, path) => {
|
||||
const type = mime_.contentType(path_.extname(path));
|
||||
if (type) {
|
||||
res.set('Content-Type', type);
|
||||
}
|
||||
},
|
||||
});
|
||||
staticFileMiddleware(req, res, next);
|
||||
return;
|
||||
}
|
||||
res.status(404).end();
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
// register routes
|
||||
registerCollectionRoutes($app);
|
||||
registerSubscriptionRoutes($app);
|
||||
registerDownloadRoutes($app);
|
||||
registerPreviewRoutes($app);
|
||||
registerSortingRoutes($app);
|
||||
registerSettingRoutes($app);
|
||||
registerArtifactRoutes($app);
|
||||
registerFileRoutes($app);
|
||||
registerTokenRoutes($app);
|
||||
registerModuleRoutes($app);
|
||||
registerSyncRoutes($app);
|
||||
registerNodeInfoRoutes($app);
|
||||
registerMiscRoutes($app);
|
||||
registerParserRoutes($app);
|
||||
|
||||
$app.start();
|
||||
|
||||
if ($.env.isNode) {
|
||||
// Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON
|
||||
const backend_sync_cron = eval(
|
||||
'process.env.SUB_STORE_BACKEND_SYNC_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
|
||||
);
|
||||
} else {
|
||||
if (eval('process.env.SUB_STORE_BACKEND_CRON')) {
|
||||
$.error(
|
||||
`[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
|
||||
);
|
||||
}
|
||||
if (eval('process.env.SUB_STORE_CRON')) {
|
||||
$.error(
|
||||
`[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// 格式: 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 mmdb_cron = eval('process.env.SUB_STORE_MMDB_CRON');
|
||||
const countryFile = eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
|
||||
const countryUrl = eval('process.env.SUB_STORE_MMDB_COUNTRY_URL');
|
||||
const asnFile = eval('process.env.SUB_STORE_MMDB_ASN_PATH');
|
||||
const asnUrl = eval('process.env.SUB_STORE_MMDB_ASN_URL');
|
||||
if (mmdb_cron && ((countryFile && countryUrl) || (asnFile && asnUrl))) {
|
||||
$.info(`[MMDB CRON] ${mmdb_cron} enabled`);
|
||||
const { CronJob } = eval(`require("cron")`);
|
||||
new CronJob(
|
||||
mmdb_cron,
|
||||
async function () {
|
||||
try {
|
||||
$.info(`[MMDB CRON] ${mmdb_cron} started`);
|
||||
if (countryFile && countryUrl) {
|
||||
try {
|
||||
$.info(
|
||||
`[MMDB CRON] downloading ${countryUrl} to ${countryFile}`,
|
||||
);
|
||||
await downloadFile(countryUrl, countryFile);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[MMDB CRON] ${countryUrl} download failed: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (asnFile && asnUrl) {
|
||||
try {
|
||||
$.info(
|
||||
`[MMDB CRON] downloading ${asnUrl} to ${asnFile}`,
|
||||
);
|
||||
await downloadFile(asnUrl, asnFile);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[MMDB CRON] ${asnUrl} download failed: ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$.info(`[MMDB CRON] ${mmdb_cron} finished`);
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[MMDB CRON] ${mmdb_cron} error: ${e.message ?? e}`,
|
||||
);
|
||||
}
|
||||
}, // onTick
|
||||
null, // onComplete
|
||||
true, // start
|
||||
// 'Asia/Shanghai' // timeZone
|
||||
);
|
||||
}
|
||||
const path = eval(`require("path")`);
|
||||
const fs = eval(`require("fs")`);
|
||||
const data_url = eval('process.env.SUB_STORE_DATA_URL');
|
||||
const data_url_post = eval('process.env.SUB_STORE_DATA_URL_POST');
|
||||
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_host =
|
||||
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
|
||||
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
|
||||
const fe_abs_path = path.resolve(
|
||||
fe_path || path.join(__dirname, 'frontend'),
|
||||
);
|
||||
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
|
||||
if (fe_path && !be_merge) {
|
||||
try {
|
||||
fs.accessSync(path.join(fe_abs_path, 'index.html'));
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
|
||||
);
|
||||
}
|
||||
|
||||
const express_ = eval(`require("express")`);
|
||||
const history = eval(`require("connect-history-api-fallback")`);
|
||||
const { createProxyMiddleware } = eval(
|
||||
`require("http-proxy-middleware")`,
|
||||
);
|
||||
|
||||
const app = express_();
|
||||
|
||||
const staticFileMiddleware = express_.static(fe_path);
|
||||
|
||||
let be_api = '/api/';
|
||||
let be_download = '/download/';
|
||||
let be_share = '/share/';
|
||||
let be_download_rewrite = '';
|
||||
let be_api_rewrite = '';
|
||||
let be_share_rewrite = `${be_share}:type/:name`;
|
||||
let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')
|
||||
? fe_be_path
|
||||
: '';
|
||||
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}${prefix}`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: async (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 req.originalUrl;
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(
|
||||
be_api_rewrite,
|
||||
createProxyMiddleware({
|
||||
target: `http://127.0.0.1:${port}${prefix}${be_api}`,
|
||||
pathRewrite: async (path) => {
|
||||
return path.includes('?')
|
||||
? `${path}&share=true`
|
||||
: `${path}?share=true`;
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(
|
||||
be_download_rewrite,
|
||||
createProxyMiddleware({
|
||||
target: `http://127.0.0.1:${port}${prefix}${be_download}`,
|
||||
changeOrigin: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
app.use(staticFileMiddleware);
|
||||
app.use(
|
||||
history({
|
||||
disableDotRule: true,
|
||||
verbose: false,
|
||||
}),
|
||||
);
|
||||
app.use(staticFileMiddleware);
|
||||
|
||||
const listener = app.listen(fe_port, fe_host, () => {
|
||||
const { address: fe_address, port: fe_port } =
|
||||
listener.address();
|
||||
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
|
||||
if (fe_be_path) {
|
||||
$.info(
|
||||
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,
|
||||
);
|
||||
$.info(
|
||||
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${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(async (content) => {
|
||||
try {
|
||||
content = JSON.parse(Base64.decode(content));
|
||||
if (Object.keys(content.settings).length === 0) {
|
||||
throw new Error(
|
||||
'备份文件应该至少包含 settings 字段',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
try {
|
||||
content = JSON.parse(content);
|
||||
if (Object.keys(content.settings).length === 0) {
|
||||
throw new Error(
|
||||
'备份文件应该至少包含 settings 字段',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Gist 备份文件校验失败, 无法还原\nReason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
throw new Error('Gist 备份文件校验失败, 无法还原');
|
||||
}
|
||||
}
|
||||
if (data_url_post) {
|
||||
$.info('[BACKEND] executing post-processing script');
|
||||
eval(data_url_post);
|
||||
}
|
||||
|
||||
$.write(JSON.stringify(content, null, ` `), '#sub-store');
|
||||
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
|
||||
migrate();
|
||||
$.info(`[BACKEND] restored data from ${data_url}`);
|
||||
})
|
||||
.catch((e) => {
|
||||
$.error(`[BACKEND] restore data failed`);
|
||||
console.error(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
272
backend/src/restful/miscs.js
Normal file
272
backend/src/restful/miscs.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
import _ from 'lodash';
|
||||
import $ from '@/core/app';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { updateArtifactStore, updateAvatar } from '@/restful/settings';
|
||||
import resourceCache from '@/utils/resource-cache';
|
||||
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||
import headersResourceCache from '@/utils/headers-resource-cache';
|
||||
import {
|
||||
GIST_BACKUP_FILE_NAME,
|
||||
GIST_BACKUP_KEY,
|
||||
SETTINGS_KEY,
|
||||
} from '@/constants';
|
||||
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
|
||||
import Gist from '@/utils/gist';
|
||||
import migrate from '@/utils/migration';
|
||||
import env from '@/utils/env';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
export default function register($app) {
|
||||
// utils
|
||||
$app.get('/api/utils/env', getEnv); // get runtime environment
|
||||
$app.get('/api/utils/backup', gistBackup); // gist backup actions
|
||||
$app.get('/api/utils/refresh', refresh);
|
||||
|
||||
// Storage management
|
||||
$app.route('/api/storage')
|
||||
.get((req, res) => {
|
||||
res.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_data_${formatDateTime(new Date())}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(
|
||||
$.env.isNode
|
||||
? JSON.stringify($.cache)
|
||||
: $.read('#sub-store'),
|
||||
);
|
||||
})
|
||||
.post((req, res) => {
|
||||
let { content } = req.body;
|
||||
try {
|
||||
content = JSON.parse(Base64.decode(content));
|
||||
if (Object.keys(content.settings).length === 0) {
|
||||
throw new Error('备份文件应该至少包含 settings 字段');
|
||||
}
|
||||
} catch (err) {
|
||||
try {
|
||||
content = JSON.parse(content);
|
||||
if (Object.keys(content.settings).length === 0) {
|
||||
throw new Error('备份文件应该至少包含 settings 字段');
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`备份文件校验失败, 无法还原\nReason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
throw new Error('备份文件校验失败, 无法还原');
|
||||
}
|
||||
}
|
||||
$.write(JSON.stringify(content, null, ` `), '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
migrate();
|
||||
success(res);
|
||||
});
|
||||
|
||||
if (ENV().isNode) {
|
||||
$app.get('/', getEnv);
|
||||
} else {
|
||||
// Redirect sub.store to vercel webpage
|
||||
$app.get('/', async (req, res) => {
|
||||
// 302 redirect
|
||||
res.set('location', 'https://sub-store.vercel.app/')
|
||||
.status(302)
|
||||
.end();
|
||||
});
|
||||
}
|
||||
|
||||
// handle preflight request for QX
|
||||
if (ENV().isQX) {
|
||||
$app.options('/', async (req, res) => {
|
||||
res.status(200).end();
|
||||
});
|
||||
}
|
||||
|
||||
$app.all('/', (_, res) => {
|
||||
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
|
||||
});
|
||||
}
|
||||
|
||||
function getEnv(req, res) {
|
||||
if (req.query.share) {
|
||||
env.feature.share = true;
|
||||
}
|
||||
res.set('Content-Type', 'application/json;charset=UTF-8').send(
|
||||
JSON.stringify(
|
||||
{
|
||||
status: 'success',
|
||||
data: {
|
||||
guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',
|
||||
...env,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function refresh(_, res) {
|
||||
// 1. get GitHub avatar and artifact store
|
||||
await updateAvatar();
|
||||
await updateArtifactStore();
|
||||
|
||||
// 2. clear resource cache
|
||||
resourceCache.revokeAll();
|
||||
scriptResourceCache.revokeAll();
|
||||
headersResourceCache.revokeAll();
|
||||
success(res);
|
||||
}
|
||||
|
||||
async function gistBackupAction(action, keep, encode) {
|
||||
// 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 currentContent = $.read('#sub-store');
|
||||
currentContent = currentContent ? JSON.parse(currentContent) : {};
|
||||
if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache));
|
||||
let content;
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const updated = settings.syncTime;
|
||||
switch (action) {
|
||||
case 'upload':
|
||||
try {
|
||||
content = $.read('#sub-store');
|
||||
content = content ? JSON.parse(content) : {};
|
||||
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
|
||||
if (encode === 'plaintext') {
|
||||
content.settings.gistToken =
|
||||
'恢复后请重新设置 GitHub Token';
|
||||
content = JSON.stringify(content, null, ` `);
|
||||
} else {
|
||||
content = Base64.encode(
|
||||
JSON.stringify(content, 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');
|
||||
content = content ? JSON.parse(content) : {};
|
||||
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
|
||||
if (encode === 'plaintext') {
|
||||
content.settings.gistToken = '恢复后请重新设置 GitHub Token';
|
||||
content = JSON.stringify(content, null, ` `);
|
||||
} else {
|
||||
content = Base64.encode(JSON.stringify(content, 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 {
|
||||
content = JSON.parse(Base64.decode(content));
|
||||
if (Object.keys(content.settings).length === 0) {
|
||||
throw new Error('备份文件应该至少包含 settings 字段');
|
||||
}
|
||||
} catch (err) {
|
||||
try {
|
||||
content = JSON.parse(content);
|
||||
if (Object.keys(content.settings).length === 0) {
|
||||
throw new Error('备份文件应该至少包含 settings 字段');
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Gist 备份文件校验失败, 无法还原\nReason: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
throw new Error('Gist 备份文件校验失败, 无法还原');
|
||||
}
|
||||
}
|
||||
if (keep) {
|
||||
$.info(`保留原有设置 ${keep}`);
|
||||
keep.split(',').forEach((path) => {
|
||||
_.set(content, path, _.get(currentContent, path));
|
||||
});
|
||||
}
|
||||
// restore settings
|
||||
$.write(JSON.stringify(content, null, ` `), '#sub-store');
|
||||
if ($.env.isNode) {
|
||||
$.cache = content;
|
||||
$.persistCache();
|
||||
}
|
||||
$.info(`perform migration after restoring from gist...`);
|
||||
migrate();
|
||||
$.info(`migration completed`);
|
||||
$.info(`还原备份完成`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
async function gistBackup(req, res) {
|
||||
const { action, keep, encode } = req.query;
|
||||
// read token
|
||||
const { gistToken } = $.read(SETTINGS_KEY);
|
||||
if (!gistToken) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'GIST_TOKEN_NOT_FOUND',
|
||||
`GitHub Token is required for backup!`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await gistBackupAction(action, keep, encode);
|
||||
success(res);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Failed to ${action} gist data.\nReason: ${err.message ?? err}`,
|
||||
);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'BACKUP_FAILED',
|
||||
`Failed to ${action} gist data!`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { gistBackupAction };
|
||||
113
backend/src/restful/module.js
Normal file
113
backend/src/restful/module.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import { MODULES_KEY } from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import $ from '@/core/app';
|
||||
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
|
||||
export default function register($app) {
|
||||
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
|
||||
|
||||
$app.route('/api/module/:name')
|
||||
.get(getModule)
|
||||
.patch(updateModule)
|
||||
.delete(deleteModule);
|
||||
|
||||
$app.route('/api/modules')
|
||||
.get(getAllModules)
|
||||
.post(createModule)
|
||||
.put(replaceModule);
|
||||
}
|
||||
|
||||
// module API
|
||||
function createModule(req, res) {
|
||||
const module = req.body;
|
||||
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
|
||||
$.info(`正在创建模块:${module.name}`);
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
if (findByName(allModules, module.name)) {
|
||||
return failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
req.body.name
|
||||
? `已存在 name 为 ${module.name} 的模块`
|
||||
: `已存在相同的模块 请勿重复添加`,
|
||||
),
|
||||
);
|
||||
}
|
||||
allModules.push(module);
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res, module, 201);
|
||||
}
|
||||
|
||||
function getModule(req, res) {
|
||||
let { name } = req.params;
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
const module = findByName(allModules, name);
|
||||
if (module) {
|
||||
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
||||
module.content,
|
||||
);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
`MODULE_NOT_FOUND`,
|
||||
`Module ${name} does not exist`,
|
||||
404,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateModule(req, res) {
|
||||
let { name } = req.params;
|
||||
let module = req.body;
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
const oldModule = findByName(allModules, name);
|
||||
if (oldModule) {
|
||||
const newModule = {
|
||||
...oldModule,
|
||||
...module,
|
||||
};
|
||||
$.info(`正在更新模块:${name}...`);
|
||||
|
||||
updateByName(allModules, name, newModule);
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res, newModule);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Module ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteModule(req, res) {
|
||||
let { name } = req.params;
|
||||
$.info(`正在删除模块:${name}`);
|
||||
let allModules = $.read(MODULES_KEY);
|
||||
deleteByName(allModules, name);
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
function getAllModules(req, res) {
|
||||
const allModules = $.read(MODULES_KEY);
|
||||
success(
|
||||
res,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
allModules.map(({ content, ...rest }) => rest),
|
||||
);
|
||||
}
|
||||
|
||||
function replaceModule(req, res) {
|
||||
const allModules = req.body;
|
||||
$.write(allModules, MODULES_KEY);
|
||||
success(res);
|
||||
}
|
||||
59
backend/src/restful/node-info.js
Normal file
59
backend/src/restful/node-info.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import producer from '@/core/proxy-utils/producers';
|
||||
import { HTTP } from '@/vendor/open-api';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { NetworkError } from '@/restful/errors';
|
||||
|
||||
export default function register($app) {
|
||||
$app.post('/api/utils/node-info', getNodeInfo);
|
||||
}
|
||||
|
||||
async function getNodeInfo(req, res) {
|
||||
const proxy = req.body;
|
||||
const lang = req.query.lang || 'zh-CN';
|
||||
let shareUrl;
|
||||
try {
|
||||
shareUrl = producer.URI.produce(proxy);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
const $http = HTTP();
|
||||
const info = await $http
|
||||
.get({
|
||||
url: `http://ip-api.com/json/${encodeURIComponent(
|
||||
`${proxy.server}`
|
||||
.trim()
|
||||
.replace(/^\[/, '')
|
||||
.replace(/\]$/, ''),
|
||||
)}?lang=${lang}`,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15',
|
||||
},
|
||||
})
|
||||
.then((resp) => {
|
||||
const data = JSON.parse(resp.body);
|
||||
if (data.status !== 'success') {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
// remove unnecessary fields
|
||||
delete data.status;
|
||||
return data;
|
||||
});
|
||||
success(res, {
|
||||
shareUrl,
|
||||
info,
|
||||
});
|
||||
} catch (err) {
|
||||
failed(
|
||||
res,
|
||||
new NetworkError(
|
||||
'FAILED_TO_GET_NODE_INFO',
|
||||
`Failed to get node info`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
368
backend/src/restful/preview.js
Normal file
368
backend/src/restful/preview.js
Normal file
@@ -0,0 +1,368 @@
|
||||
import { InternalServerError } from './errors';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { findByName } from '@/utils/database';
|
||||
import { success, failed } from './response';
|
||||
import download from '@/utils/download';
|
||||
import { SUBS_KEY } from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function register($app) {
|
||||
$app.post('/api/preview/sub', compareSub);
|
||||
$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,
|
||||
undefined,
|
||||
file.proxy,
|
||||
);
|
||||
} catch (err) {
|
||||
errors[url] = err;
|
||||
$.error(
|
||||
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
|
||||
);
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
if (!file.ignoreFailedRemoteFile) {
|
||||
throw new Error(
|
||||
`文件 ${file.name} 的远程文件 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (file.ignoreFailedRemoteFile === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 预览文件失败`,
|
||||
`❌ ${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) {
|
||||
try {
|
||||
const sub = req.body;
|
||||
const target = req.query.target || 'JSON';
|
||||
let content;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
content = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
content = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.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 (Object.keys(errors).length > 0) {
|
||||
if (!sub.ignoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (sub.ignoreFailedRemoteSub === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 预览订阅失败`,
|
||||
`❌ ${sub.name}`,
|
||||
`远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
content.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
content.push(sub.content);
|
||||
}
|
||||
}
|
||||
// parse proxies
|
||||
const original = (Array.isArray(content) ? content : [content])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
// add id
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
proxy._subName = sub.name;
|
||||
proxy._subDisplayName = sub.displayName;
|
||||
});
|
||||
|
||||
// apply processors
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
sub.process || [],
|
||||
target,
|
||||
{ [sub.name]: sub },
|
||||
);
|
||||
|
||||
// produce
|
||||
success(res, { original, processed });
|
||||
} catch (err) {
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`INTERNAL_SERVER_ERROR`,
|
||||
`Failed to preview subscription`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function compareCollection(req, res) {
|
||||
try {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const collection = req.body;
|
||||
const subnames = [...collection.subscriptions];
|
||||
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 errors = {};
|
||||
await Promise.all(
|
||||
subnames.map(async (name) => {
|
||||
const sub = findByName(allSubs, name);
|
||||
try {
|
||||
let raw;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(
|
||||
sub.mergeSources,
|
||||
)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.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 (Object.keys(errors).length > 0) {
|
||||
if (!sub.ignoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (
|
||||
sub.ignoreFailedRemoteSub === 'enabled'
|
||||
) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 预览订阅失败`,
|
||||
`❌ ${sub.name}`,
|
||||
`远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 (Object.keys(errors).length > 0) {
|
||||
if (!collection.ignoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`组合订阅 ${collection.name} 的子订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (collection.ignoreFailedRemoteSub === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 预览组合订阅失败`,
|
||||
`❌ ${collection.name}`,
|
||||
`子订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// merge proxies with the original order
|
||||
const original = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
|
||||
original.forEach((proxy, i) => {
|
||||
proxy.id = i;
|
||||
proxy._collectionName = collection.name;
|
||||
proxy._collectionDisplayName = collection.displayName;
|
||||
});
|
||||
|
||||
const processed = await ProxyUtils.process(
|
||||
original,
|
||||
collection.process || [],
|
||||
'JSON',
|
||||
{ _collection: collection },
|
||||
);
|
||||
|
||||
success(res, { original, processed });
|
||||
} catch (err) {
|
||||
$.error(err.message ?? err);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`INTERNAL_SERVER_ERROR`,
|
||||
`Failed to preview collection`,
|
||||
`Reason: ${err.message ?? err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
backend/src/restful/response.js
Normal file
20
backend/src/restful/response.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export function success(resp, data, statusCode) {
|
||||
resp.status(statusCode || 200).json({
|
||||
status: 'success',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function failed(resp, error, statusCode) {
|
||||
resp.status(statusCode || 500).json({
|
||||
status: 'failed',
|
||||
error: {
|
||||
code: error.code,
|
||||
type: error.type,
|
||||
message: error.message,
|
||||
details: resp.req?.route?.path?.startsWith('/share/')
|
||||
? '详情请查看日志'
|
||||
: error.details,
|
||||
},
|
||||
});
|
||||
}
|
||||
152
backend/src/restful/settings.js
Normal file
152
backend/src/restful/settings.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
|
||||
import { success, failed } from './response';
|
||||
import { InternalServerError } from '@/restful/errors';
|
||||
import $ from '@/core/app';
|
||||
import Gist from '@/utils/gist';
|
||||
|
||||
export default function register($app) {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) $.write({}, SETTINGS_KEY);
|
||||
$app.route('/api/settings').get(getSettings).patch(updateSettings);
|
||||
}
|
||||
|
||||
async function getSettings(req, res) {
|
||||
try {
|
||||
let settings = $.read(SETTINGS_KEY);
|
||||
if (!settings) {
|
||||
settings = {};
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
|
||||
if (!settings.avatarUrl) await updateAvatar();
|
||||
if (!settings.artifactStore) await updateArtifactStore();
|
||||
|
||||
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) {
|
||||
try {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const newSettings = {
|
||||
...settings,
|
||||
...req.body,
|
||||
};
|
||||
$.write(newSettings, SETTINGS_KEY);
|
||||
await updateAvatar();
|
||||
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 updateAvatar() {
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const { githubUser: username, syncPlatform, githubProxy } = settings;
|
||||
if (username) {
|
||||
if (syncPlatform === 'gitlab') {
|
||||
try {
|
||||
const data = await $.http
|
||||
.get({
|
||||
url: `https://gitlab.com/api/v4/users?username=${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[0]['avatar_url'].replace(
|
||||
/(\?|&)s=\d+(&|$)/,
|
||||
'$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: `${
|
||||
githubProxy ? `${githubProxy}/` : ''
|
||||
}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
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateArtifactStore() {
|
||||
$.log('Updating artifact store');
|
||||
const settings = $.read(SETTINGS_KEY);
|
||||
const { gistToken, syncPlatform } = settings;
|
||||
if (gistToken) {
|
||||
const manager = new Gist({
|
||||
token: gistToken,
|
||||
key: ARTIFACT_REPOSITORY_KEY,
|
||||
syncPlatform,
|
||||
});
|
||||
|
||||
try {
|
||||
const gist = await manager.locate();
|
||||
const url = gist?.html_url ?? gist?.web_url;
|
||||
if (url) {
|
||||
$.log(`找到 Sub-Store Gist: ${url}`);
|
||||
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
|
||||
settings.artifactStore = url;
|
||||
settings.artifactStoreStatus = 'VALID';
|
||||
} else {
|
||||
$.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);
|
||||
settings.artifactStoreStatus = 'NOT FOUND';
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${
|
||||
err.message ?? err
|
||||
}`,
|
||||
);
|
||||
settings.artifactStoreStatus = 'ERROR';
|
||||
}
|
||||
$.write(settings, SETTINGS_KEY);
|
||||
}
|
||||
}
|
||||
49
backend/src/restful/sort.js
Normal file
49
backend/src/restful/sort.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
ARTIFACTS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
SUBS_KEY,
|
||||
FILES_KEY,
|
||||
} from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
import { success } from '@/restful/response';
|
||||
|
||||
export default function register($app) {
|
||||
$app.post('/api/sort/subs', sortSubs);
|
||||
$app.post('/api/sort/collections', sortCollections);
|
||||
$app.post('/api/sort/artifacts', sortArtifacts);
|
||||
$app.post('/api/sort/files', sortFiles);
|
||||
}
|
||||
|
||||
function sortSubs(req, res) {
|
||||
const orders = req.body;
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res, allSubs);
|
||||
}
|
||||
|
||||
function sortCollections(req, res) {
|
||||
const orders = req.body;
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res, allCols);
|
||||
}
|
||||
|
||||
function sortArtifacts(req, res) {
|
||||
const orders = req.body;
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
allArtifacts.sort(
|
||||
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
|
||||
);
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
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);
|
||||
}
|
||||
384
backend/src/restful/subscriptions.js
Normal file
384
backend/src/restful/subscriptions.js
Normal file
@@ -0,0 +1,384 @@
|
||||
import {
|
||||
NetworkError,
|
||||
InternalServerError,
|
||||
ResourceNotFoundError,
|
||||
RequestInvalidError,
|
||||
} from './errors';
|
||||
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
||||
import {
|
||||
SUBS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
ARTIFACTS_KEY,
|
||||
FILES_KEY,
|
||||
} from '@/constants';
|
||||
import {
|
||||
getFlowHeaders,
|
||||
parseFlowHeaders,
|
||||
getRmainingDays,
|
||||
} from '@/utils/flow';
|
||||
import { success, failed } from './response';
|
||||
import $ from '@/core/app';
|
||||
import { formatDateTime } from '@/utils';
|
||||
|
||||
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
|
||||
|
||||
export default function register($app) {
|
||||
$app.get('/api/sub/flow/:name', getFlowInfo);
|
||||
|
||||
$app.route('/api/sub/:name')
|
||||
.get(getSubscription)
|
||||
.patch(updateSubscription)
|
||||
.delete(deleteSubscription);
|
||||
|
||||
$app.route('/api/subs')
|
||||
.get(getAllSubscriptions)
|
||||
.post(createSubscription)
|
||||
.put(replaceSubscriptions);
|
||||
}
|
||||
|
||||
// subscriptions API
|
||||
async function getFlowInfo(req, res) {
|
||||
let { name } = req.params;
|
||||
let { url } = req.query;
|
||||
if (url) {
|
||||
$.info(`指定远程订阅 URL: ${url}`);
|
||||
}
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
if (!sub) {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Subscription ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
if (sub.subUserinfo) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
try {
|
||||
url =
|
||||
`${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 || !/^https?/.test(url)) {
|
||||
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(
|
||||
res,
|
||||
new InternalServerError(
|
||||
'NO_FLOW_INFO',
|
||||
'No flow info',
|
||||
`Failed to fetch flow headers`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
failed(
|
||||
res,
|
||||
new NetworkError(
|
||||
`URL_NOT_ACCESSIBLE`,
|
||||
`The URL for subscription ${name} is inaccessible.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createSubscription(req, res) {
|
||||
const sub = req.body;
|
||||
delete sub.subscriptions;
|
||||
$.info(`正在创建订阅: ${sub.name}`);
|
||||
if (/\//.test(sub.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_NAME',
|
||||
`Subscription ${sub.name} is invalid`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
if (findByName(allSubs, sub.name)) {
|
||||
failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_KEY',
|
||||
`Subscription ${sub.name} already exists.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
allSubs.push(sub);
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res, sub, 201);
|
||||
}
|
||||
|
||||
function getSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
let { raw } = req.query;
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const sub = findByName(allSubs, name);
|
||||
delete sub.subscriptions;
|
||||
if (sub) {
|
||||
if (raw) {
|
||||
res.set('content-type', 'application/json')
|
||||
.set(
|
||||
'content-disposition',
|
||||
`attachment; filename="${encodeURIComponent(
|
||||
`sub-store_subscription_${name}_${formatDateTime(
|
||||
new Date(),
|
||||
)}.json`,
|
||||
)}"`,
|
||||
)
|
||||
.send(JSON.stringify(sub));
|
||||
} else {
|
||||
success(res, sub);
|
||||
}
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
`SUBSCRIPTION_NOT_FOUND`,
|
||||
`Subscription ${name} does not exist`,
|
||||
404,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
let sub = req.body;
|
||||
delete sub.subscriptions;
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const oldSub = findByName(allSubs, name);
|
||||
if (oldSub) {
|
||||
const newSub = {
|
||||
...oldSub,
|
||||
...sub,
|
||||
};
|
||||
$.info(`正在更新订阅: ${name}`);
|
||||
// allow users to update the subscription name
|
||||
if (name !== sub.name) {
|
||||
// update all collections refer to this name
|
||||
const allCols = $.read(COLLECTIONS_KEY) || [];
|
||||
for (const collection of allCols) {
|
||||
const idx = collection.subscriptions.indexOf(name);
|
||||
if (idx !== -1) {
|
||||
collection.subscriptions[idx] = sub.name;
|
||||
}
|
||||
}
|
||||
|
||||
// update all artifacts referring this subscription
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
|
||||
for (const artifact of allArtifacts) {
|
||||
if (
|
||||
artifact.type === 'subscription' &&
|
||||
artifact.source == name
|
||||
) {
|
||||
artifact.source = sub.name;
|
||||
}
|
||||
}
|
||||
// update all files referring this subscription
|
||||
const allFiles = $.read(FILES_KEY) || [];
|
||||
for (const file of allFiles) {
|
||||
if (
|
||||
file.sourceType === 'subscription' &&
|
||||
file.sourceName == name
|
||||
) {
|
||||
file.sourceName = sub.name;
|
||||
}
|
||||
}
|
||||
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||
$.write(allFiles, FILES_KEY);
|
||||
}
|
||||
updateByName(allSubs, name, newSub);
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res, newSub);
|
||||
} else {
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`Subscription ${name} does not exist!`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSubscription(req, res) {
|
||||
let { name } = req.params;
|
||||
$.info(`删除订阅:${name}...`);
|
||||
// delete from subscriptions
|
||||
let allSubs = $.read(SUBS_KEY);
|
||||
deleteByName(allSubs, name);
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
// delete from collections
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
for (const collection of allCols) {
|
||||
collection.subscriptions = collection.subscriptions.filter(
|
||||
(s) => s !== name,
|
||||
);
|
||||
}
|
||||
$.write(allCols, COLLECTIONS_KEY);
|
||||
success(res);
|
||||
}
|
||||
|
||||
function getAllSubscriptions(req, res) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
success(res, allSubs);
|
||||
}
|
||||
|
||||
function replaceSubscriptions(req, res) {
|
||||
const allSubs = req.body;
|
||||
$.write(allSubs, SUBS_KEY);
|
||||
success(res);
|
||||
}
|
||||
900
backend/src/restful/sync.js
Normal file
900
backend/src/restful/sync.js
Normal file
@@ -0,0 +1,900 @@
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
ARTIFACTS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
RULES_KEY,
|
||||
SUBS_KEY,
|
||||
FILES_KEY,
|
||||
} from '@/constants';
|
||||
import { failed, success } from '@/restful/response';
|
||||
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
|
||||
import { findByName } from '@/utils/database';
|
||||
import download from '@/utils/download';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
import { RuleUtils } from '@/core/rule-utils';
|
||||
import { syncToGist } from '@/restful/artifacts';
|
||||
|
||||
export default function register($app) {
|
||||
// Initialization
|
||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||
|
||||
// sync all artifacts
|
||||
$app.get('/api/sync/artifacts', syncAllArtifacts);
|
||||
$app.get('/api/sync/artifact/:name', syncArtifact);
|
||||
}
|
||||
|
||||
async function produceArtifact({
|
||||
type,
|
||||
name,
|
||||
platform,
|
||||
url,
|
||||
ua,
|
||||
content,
|
||||
mergeSources,
|
||||
ignoreFailedRemoteSub,
|
||||
ignoreFailedRemoteFile,
|
||||
produceType,
|
||||
produceOpts = {},
|
||||
subscription,
|
||||
awaitCustomCache,
|
||||
$options,
|
||||
proxy,
|
||||
noCache,
|
||||
all,
|
||||
}) {
|
||||
platform = platform || 'JSON';
|
||||
|
||||
if (['subscription', 'sub'].includes(type)) {
|
||||
let sub;
|
||||
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;
|
||||
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 || 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 (Object.keys(errors).length > 0) {
|
||||
if (!subIgnoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (subIgnoreFailedRemoteSub === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 处理订阅失败`,
|
||||
`❌ ${sub.name}`,
|
||||
`远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (mergeSources === 'localFirst') {
|
||||
raw.unshift(content);
|
||||
} else if (mergeSources === 'remoteFirst') {
|
||||
raw.push(content);
|
||||
}
|
||||
} else if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.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 (Object.keys(errors).length > 0) {
|
||||
if (!subIgnoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (subIgnoreFailedRemoteSub === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 处理订阅失败`,
|
||||
`❌ ${sub.name}`,
|
||||
`远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (sub.mergeSources === 'localFirst') {
|
||||
raw.unshift(sub.content);
|
||||
} else if (sub.mergeSources === 'remoteFirst') {
|
||||
raw.push(sub.content);
|
||||
}
|
||||
}
|
||||
if (produceType === 'raw') {
|
||||
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
|
||||
}
|
||||
// parse proxies
|
||||
let proxies = (Array.isArray(raw) ? raw : [raw])
|
||||
.map((i) => ProxyUtils.parse(i))
|
||||
.flat();
|
||||
|
||||
proxies.forEach((proxy) => {
|
||||
proxy._subName = sub.name;
|
||||
proxy._subDisplayName = sub.displayName;
|
||||
});
|
||||
// apply processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
sub.process || [],
|
||||
platform,
|
||||
{ [sub.name]: sub },
|
||||
$options,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`订阅 ${name} 中不含有效节点`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
if (exist[proxy.name]) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
`⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}!`,
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'media-url':
|
||||
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
exist[proxy.name] = true;
|
||||
}
|
||||
// produce
|
||||
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||
} else if (['collection', 'col'].includes(type)) {
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const collection = findByName(allCols, name);
|
||||
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 errors = {};
|
||||
let processed = 0;
|
||||
|
||||
await Promise.all(
|
||||
subnames.map(async (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 {
|
||||
$.info(`正在处理子订阅:${sub.name}...`);
|
||||
let raw;
|
||||
if (
|
||||
sub.source === 'local' &&
|
||||
!['localFirst', 'remoteFirst'].includes(
|
||||
sub.mergeSources,
|
||||
)
|
||||
) {
|
||||
raw = sub.content;
|
||||
} else {
|
||||
const errors = {};
|
||||
raw = await await Promise.all(
|
||||
sub.url
|
||||
.split(/[\r\n]+/)
|
||||
.map((i) => i.trim())
|
||||
.filter((i) => i.length)
|
||||
.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 (Object.keys(errors).length > 0) {
|
||||
if (!sub.ignoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (
|
||||
sub.ignoreFailedRemoteSub === 'enabled'
|
||||
) {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 处理订阅失败`,
|
||||
`❌ ${sub.name}`,
|
||||
`远程订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 || [],
|
||||
platform,
|
||||
{
|
||||
[sub.name]: sub,
|
||||
_collection: collection,
|
||||
$options,
|
||||
},
|
||||
);
|
||||
results[name] = currentProxies;
|
||||
processed++;
|
||||
$.info(
|
||||
`✅ 子订阅:${sub.name}加载成功,进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}% `,
|
||||
);
|
||||
} catch (err) {
|
||||
processed++;
|
||||
errors[name] = err;
|
||||
$.error(
|
||||
`❌ 处理组合订阅中的子订阅: ${
|
||||
sub.name
|
||||
}时出现错误:${err}!进度--${
|
||||
100 * (processed / subnames.length).toFixed(1)
|
||||
}%`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
|
||||
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
|
||||
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
if (!collectionIgnoreFailedRemoteSub) {
|
||||
throw new Error(
|
||||
`组合订阅 ${collection.name} 的子订阅 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (collectionIgnoreFailedRemoteSub === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 处理组合订阅失败`,
|
||||
`❌ ${collection.name}`,
|
||||
`子订阅 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// merge proxies with the original order
|
||||
let proxies = Array.prototype.concat.apply(
|
||||
[],
|
||||
subnames.map((name) => results[name] || []),
|
||||
);
|
||||
|
||||
proxies.forEach((proxy) => {
|
||||
proxy._collectionName = collection.name;
|
||||
proxy._collectionDisplayName = collection.displayName;
|
||||
});
|
||||
|
||||
// apply own processors
|
||||
proxies = await ProxyUtils.process(
|
||||
proxies,
|
||||
collection.process || [],
|
||||
platform,
|
||||
{ _collection: collection },
|
||||
$options,
|
||||
);
|
||||
if (proxies.length === 0) {
|
||||
throw new Error(`组合订阅 ${name} 中不含有效节点`);
|
||||
}
|
||||
// check duplicate
|
||||
const exist = {};
|
||||
for (const proxy of proxies) {
|
||||
if (exist[proxy.name]) {
|
||||
$.notify(
|
||||
'🌍 Sub-Store',
|
||||
`⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}!`,
|
||||
'请仔细检测配置!',
|
||||
{
|
||||
'media-url':
|
||||
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
exist[proxy.name] = true;
|
||||
}
|
||||
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
|
||||
} else if (type === 'rule') {
|
||||
const allRules = $.read(RULES_KEY);
|
||||
const rule = findByName(allRules, name);
|
||||
if (!rule) throw new Error(`找不到规则 ${name}`);
|
||||
let rules = [];
|
||||
for (let i = 0; i < rule.urls.length; i++) {
|
||||
const url = rule.urls[i];
|
||||
$.info(
|
||||
`正在处理URL:${url},进度--${
|
||||
100 * ((i + 1) / rule.urls.length).toFixed(1)
|
||||
}% `,
|
||||
);
|
||||
try {
|
||||
const { body } = await download(url);
|
||||
const currentRules = RuleUtils.parse(body);
|
||||
rules = rules.concat(currentRules);
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// remove duplicates
|
||||
rules = await RuleUtils.process(rules, [
|
||||
{ type: 'Remove Duplicate Filter' },
|
||||
]);
|
||||
// produce output
|
||||
return RuleUtils.produce(rules, platform);
|
||||
} else if (type === 'file') {
|
||||
const allFiles = $.read(FILES_KEY);
|
||||
const file = findByName(allFiles, name);
|
||||
if (!file) throw new Error(`找不到文件 ${name}`);
|
||||
let raw = '';
|
||||
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 (Object.keys(errors).length > 0) {
|
||||
if (!fileIgnoreFailedRemoteFile) {
|
||||
throw new Error(
|
||||
`文件 ${file.name} 的远程文件 ${Object.keys(
|
||||
errors,
|
||||
).join(', ')} 发生错误, 请查看日志`,
|
||||
);
|
||||
} else if (fileIgnoreFailedRemoteFile === 'enabled') {
|
||||
$.notify(
|
||||
`🌍 Sub-Store 处理文件失败`,
|
||||
`❌ ${file.name}`,
|
||||
`远程文件 ${Object.keys(errors).join(
|
||||
', ',
|
||||
)} 发生错误, 请查看日志`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (file.mergeSources === 'localFirst') {
|
||||
raw.unshift(file.content);
|
||||
} else if (file.mergeSources === 'remoteFirst') {
|
||||
raw.push(file.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (produceType === 'raw') {
|
||||
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
|
||||
}
|
||||
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 (all ? processed : processed?.$content) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
async function syncArtifacts() {
|
||||
$.info('开始同步所有远程配置...');
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const files = {};
|
||||
|
||||
try {
|
||||
const valid = [];
|
||||
const invalid = [];
|
||||
const allSubs = $.read(SUBS_KEY);
|
||||
const allCols = $.read(COLLECTIONS_KEY);
|
||||
const subNames = [];
|
||||
let enabledCount = 0;
|
||||
allArtifacts.map((artifact) => {
|
||||
if (artifact.sync && artifact.source) {
|
||||
enabledCount++;
|
||||
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 (enabledCount === 0) {
|
||||
$.info(
|
||||
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
allArtifacts.map(async (artifact) => {
|
||||
try {
|
||||
if (artifact.sync && artifact.source) {
|
||||
$.info(`正在同步云配置:${artifact.name}...`);
|
||||
|
||||
const useMihomoExternal =
|
||||
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 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) {
|
||||
if (
|
||||
artifact.sync &&
|
||||
artifact.source &&
|
||||
valid.includes(artifact.name)
|
||||
) {
|
||||
artifact.updated = new Date().getTime();
|
||||
// extract real url from gist
|
||||
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);
|
||||
$.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);
|
||||
} catch (e) {
|
||||
$.error(`同步配置失败,原因:${e.message ?? e}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_SYNC_ARTIFACTS`,
|
||||
`Failed to sync all artifacts`,
|
||||
`Reason: ${e.message ?? e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function syncArtifact(req, res) {
|
||||
let { name } = req.params;
|
||||
$.info(`开始同步远程配置 ${name}...`);
|
||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||
const artifact = findByName(allArtifacts, name);
|
||||
|
||||
if (!artifact) {
|
||||
$.error(`找不到远程配置 ${name}`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_NOT_FOUND',
|
||||
`找不到远程配置 ${name}`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!artifact.source) {
|
||||
$.error(`远程配置 ${name} 未设置来源`);
|
||||
failed(
|
||||
res,
|
||||
new ResourceNotFoundError(
|
||||
'RESOURCE_HAS_NO_SOURCE',
|
||||
`远程配置 ${name} 未设置来源`,
|
||||
),
|
||||
404,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const useMihomoExternal = 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,
|
||||
},
|
||||
});
|
||||
|
||||
$.info(
|
||||
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
|
||||
artifact,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
// if (!output || output.length === 0)
|
||||
// throw new Error('该配置的结果为空 不进行上传');
|
||||
const resp = await syncToGist({
|
||||
[encodeURIComponent(artifact.name)]: {
|
||||
content: output,
|
||||
},
|
||||
});
|
||||
artifact.updated = new Date().getTime();
|
||||
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));
|
||||
|
||||
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);
|
||||
success(res, artifact);
|
||||
} catch (err) {
|
||||
$.error(`远程配置 ${artifact.name} 发生错误: ${err.message ?? err}`);
|
||||
failed(
|
||||
res,
|
||||
new InternalServerError(
|
||||
`FAILED_TO_SYNC_ARTIFACT`,
|
||||
`Failed to sync artifact ${name}`,
|
||||
`Reason: ${err}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { produceArtifact, syncArtifacts };
|
||||
191
backend/src/restful/token.js
Normal file
191
backend/src/restful/token.js
Normal file
@@ -0,0 +1,191 @@
|
||||
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;
|
||||
$.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")`);
|
||||
const type = payload?.type;
|
||||
const name = payload?.name;
|
||||
if (!type || !name)
|
||||
return failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'INVALID_PAYLOAD',
|
||||
`payload type and name are required`,
|
||||
),
|
||||
);
|
||||
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 && t.type === type && t.name === name,
|
||||
)
|
||||
) {
|
||||
return failed(
|
||||
res,
|
||||
new RequestInvalidError(
|
||||
'DUPLICATE_TOKEN',
|
||||
`Token ${token} already exists`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 && t.type === type && t.name === name,
|
||||
)
|
||||
);
|
||||
}
|
||||
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}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
backend/src/test/proxy-parsers/loon.spec.js
Normal file
144
backend/src/test/proxy-parsers/loon.spec.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import getLoonParser from '@/core/proxy-utils/parsers/peggy/loon';
|
||||
import { describe, it } from 'mocha';
|
||||
import testcases from './testcases';
|
||||
import { expect } from 'chai';
|
||||
|
||||
const parser = getLoonParser();
|
||||
|
||||
describe('Loon', function () {
|
||||
describe('shadowsocks', function () {
|
||||
it('test shadowsocks simple', function () {
|
||||
const { input, expected } = testcases.SS.SIMPLE;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks obfs + tls', function () {
|
||||
const { input, expected } = testcases.SS.OBFS_TLS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks obfs + http', function () {
|
||||
const { input, expected } = testcases.SS.OBFS_HTTP;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shadowsocksr', function () {
|
||||
it('test shadowsocksr simple', function () {
|
||||
const { input, expected } = testcases.SSR.SIMPLE;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trojan', function () {
|
||||
it('test trojan simple', function () {
|
||||
const { input, expected } = testcases.TROJAN.SIMPLE;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + ws', function () {
|
||||
const { input, expected } = testcases.TROJAN.WS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + wss', function () {
|
||||
const { input, expected } = testcases.TROJAN.WSS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vmess', function () {
|
||||
it('test vmess simple', function () {
|
||||
const { input, expected } = testcases.VMESS.SIMPLE;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vmess + aead', function () {
|
||||
const { input, expected } = testcases.VMESS.AEAD;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vmess + ws', function () {
|
||||
const { input, expected } = testcases.VMESS.WS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vmess + wss', function () {
|
||||
const { input, expected } = testcases.VMESS.WSS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vmess + http', function () {
|
||||
const { input, expected } = testcases.VMESS.HTTP;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vmess + http + tls', function () {
|
||||
const { input, expected } = testcases.VMESS.HTTP_TLS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vless', function () {
|
||||
it('test vless simple', function () {
|
||||
const { input, expected } = testcases.VLESS.SIMPLE;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vless + ws', function () {
|
||||
const { input, expected } = testcases.VLESS.WS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vless + wss', function () {
|
||||
const { input, expected } = testcases.VLESS.WSS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vless + http', function () {
|
||||
const { input, expected } = testcases.VLESS.HTTP;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
|
||||
it('test vless + http + tls', function () {
|
||||
const { input, expected } = testcases.VLESS.HTTP_TLS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected.Loon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('http(s)', function () {
|
||||
it('test http simple', function () {
|
||||
const { input, expected } = testcases.HTTP.SIMPLE;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test http with authentication', function () {
|
||||
const { input, expected } = testcases.HTTP.AUTH;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test https', function () {
|
||||
const { input, expected } = testcases.HTTP.TLS;
|
||||
const proxy = parser.parse(input.Loon);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
142
backend/src/test/proxy-parsers/qx.spec.js
Normal file
142
backend/src/test/proxy-parsers/qx.spec.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import getQXParser from '@/core/proxy-utils/parsers/peggy/qx';
|
||||
import { describe, it } from 'mocha';
|
||||
import testcases from './testcases';
|
||||
import { expect } from 'chai';
|
||||
|
||||
const parser = getQXParser();
|
||||
|
||||
describe('QX', function () {
|
||||
describe('shadowsocks', function () {
|
||||
it('test shadowsocks simple', function () {
|
||||
const { input, expected } = testcases.SS.SIMPLE;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks obfs + tls', function () {
|
||||
const { input, expected } = testcases.SS.OBFS_TLS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks obfs + http', function () {
|
||||
const { input, expected } = testcases.SS.OBFS_HTTP;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks v2ray-plugin + ws', function () {
|
||||
const { input, expected } = testcases.SS.V2RAY_PLUGIN_WS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks v2ray-plugin + wss', function () {
|
||||
const { input, expected } = testcases.SS.V2RAY_PLUGIN_WSS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shadowsocksr', function () {
|
||||
it('test shadowsocksr simple', function () {
|
||||
const { input, expected } = testcases.SSR.SIMPLE;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trojan', function () {
|
||||
it('test trojan simple', function () {
|
||||
const { input, expected } = testcases.TROJAN.SIMPLE;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + ws', function () {
|
||||
const { input, expected } = testcases.TROJAN.WS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + wss', function () {
|
||||
const { input, expected } = testcases.TROJAN.WSS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + tls fingerprint', function () {
|
||||
const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vmess', function () {
|
||||
it('test vmess simple', function () {
|
||||
const { input, expected } = testcases.VMESS.SIMPLE;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected.QX);
|
||||
});
|
||||
|
||||
it('test vmess aead', function () {
|
||||
const { input, expected } = testcases.VMESS.AEAD;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected.QX);
|
||||
});
|
||||
|
||||
it('test vmess + ws', function () {
|
||||
const { input, expected } = testcases.VMESS.WS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected.QX);
|
||||
});
|
||||
|
||||
it('test vmess + wss', function () {
|
||||
const { input, expected } = testcases.VMESS.WSS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected.QX);
|
||||
});
|
||||
|
||||
it('test vmess + http', function () {
|
||||
const { input, expected } = testcases.VMESS.HTTP;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected.QX);
|
||||
});
|
||||
});
|
||||
|
||||
describe('http', function () {
|
||||
it('test http simple', function () {
|
||||
const { input, expected } = testcases.HTTP.SIMPLE;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test http with authentication', function () {
|
||||
const { input, expected } = testcases.HTTP.AUTH;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test https', function () {
|
||||
const { input, expected } = testcases.HTTP.TLS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('socks5', function () {
|
||||
it('test socks5 simple', function () {
|
||||
const { input, expected } = testcases.SOCKS5.SIMPLE;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test socks5 with authentication', function () {
|
||||
const { input, expected } = testcases.SOCKS5.AUTH;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test socks5 + tls', function () {
|
||||
const { input, expected } = testcases.SOCKS5.TLS;
|
||||
const proxy = parser.parse(input.QX);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
138
backend/src/test/proxy-parsers/surge.spec.js
Normal file
138
backend/src/test/proxy-parsers/surge.spec.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import getSurgeParser from '@/core/proxy-utils/parsers/peggy/surge';
|
||||
import { describe, it } from 'mocha';
|
||||
import testcases from './testcases';
|
||||
import { expect } from 'chai';
|
||||
|
||||
const parser = getSurgeParser();
|
||||
|
||||
describe('Surge', function () {
|
||||
describe('shadowsocks', function () {
|
||||
it('test shadowsocks simple', function () {
|
||||
const { input, expected } = testcases.SS.SIMPLE;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks obfs + tls', function () {
|
||||
const { input, expected } = testcases.SS.OBFS_TLS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
it('test shadowsocks obfs + http', function () {
|
||||
const { input, expected } = testcases.SS.OBFS_HTTP;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trojan', function () {
|
||||
it('test trojan simple', function () {
|
||||
const { input, expected } = testcases.TROJAN.SIMPLE;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + ws', function () {
|
||||
const { input, expected } = testcases.TROJAN.WS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + wss', function () {
|
||||
const { input, expected } = testcases.TROJAN.WSS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test trojan + tls fingerprint', function () {
|
||||
const { input, expected } = testcases.TROJAN.TLS_FINGERPRINT;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('vmess', function () {
|
||||
it('test vmess simple', function () {
|
||||
const { input, expected } = testcases.VMESS.SIMPLE;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected.Surge);
|
||||
});
|
||||
|
||||
it('test vmess aead', function () {
|
||||
const { input, expected } = testcases.VMESS.AEAD;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected.Surge);
|
||||
});
|
||||
|
||||
it('test vmess + ws', function () {
|
||||
const { input, expected } = testcases.VMESS.WS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected.Surge);
|
||||
});
|
||||
|
||||
it('test vmess + wss', function () {
|
||||
const { input, expected } = testcases.VMESS.WSS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected.Surge);
|
||||
});
|
||||
});
|
||||
|
||||
describe('http', function () {
|
||||
it('test http simple', function () {
|
||||
const { input, expected } = testcases.HTTP.SIMPLE;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test http with authentication', function () {
|
||||
const { input, expected } = testcases.HTTP.AUTH;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test https', function () {
|
||||
const { input, expected } = testcases.HTTP.TLS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('socks5', function () {
|
||||
it('test socks5 simple', function () {
|
||||
const { input, expected } = testcases.SOCKS5.SIMPLE;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test socks5 with authentication', function () {
|
||||
const { input, expected } = testcases.SOCKS5.AUTH;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test socks5 + tls', function () {
|
||||
const { input, expected } = testcases.SOCKS5.TLS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snell', function () {
|
||||
it('test snell simple', function () {
|
||||
const { input, expected } = testcases.SNELL.SIMPLE;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test snell obfs + http', function () {
|
||||
const { input, expected } = testcases.SNELL.OBFS_HTTP;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
|
||||
it('test snell obfs + tls', function () {
|
||||
const { input, expected } = testcases.SNELL.OBFS_TLS;
|
||||
const proxy = parser.parse(input.Surge);
|
||||
expect(proxy).eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
749
backend/src/test/proxy-parsers/testcases.js
Normal file
749
backend/src/test/proxy-parsers/testcases.js
Normal file
@@ -0,0 +1,749 @@
|
||||
function createTestCases() {
|
||||
const name = 'name';
|
||||
const server = 'example.com';
|
||||
const port = 10086;
|
||||
|
||||
const cipher = 'chacha20';
|
||||
|
||||
const username = 'username';
|
||||
const password = 'password';
|
||||
|
||||
const obfs_host = 'obfs.com';
|
||||
const obfs_path = '/resource/file';
|
||||
|
||||
const ssr_protocol = 'auth_chain_b';
|
||||
const ssr_protocol_param = 'def';
|
||||
const ssr_obfs = 'tls1.2_ticket_fastauth';
|
||||
const ssr_obfs_param = 'obfs.com';
|
||||
|
||||
const uuid = '23ad6b10-8d1a-40f7-8ad0-e3e35cd32291';
|
||||
|
||||
const sni = 'sni.com';
|
||||
|
||||
const tls_fingerprint =
|
||||
'67:1B:C8:F2:D4:60:DD:A7:EE:60:DA:BB:A3:F9:A4:D7:C8:29:0F:3E:2F:75:B6:A9:46:88:48:7D:D3:97:7E:98';
|
||||
|
||||
const SS = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
Loon: `${name}=shadowsocks,${server},${port},${cipher},"${password}"`,
|
||||
QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},tag=${name}`,
|
||||
Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'ss',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
cipher,
|
||||
password,
|
||||
},
|
||||
},
|
||||
OBFS_TLS: {
|
||||
input: {
|
||||
Loon: `${name}=shadowsocks,${server},${port},${cipher},"${password}",obfs-name=tls,obfs-uri=${obfs_path},obfs-host=${obfs_host}`,
|
||||
QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password},obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'ss',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
cipher,
|
||||
password,
|
||||
plugin: 'obfs',
|
||||
'plugin-opts': {
|
||||
mode: 'tls',
|
||||
path: obfs_path,
|
||||
host: obfs_host,
|
||||
},
|
||||
},
|
||||
},
|
||||
OBFS_HTTP: {
|
||||
input: {
|
||||
Loon: `${name}=shadowsocks,${server},${port},${cipher},"${password}",obfs-name=http,obfs-uri=${obfs_path},obfs-host=${obfs_host}`,
|
||||
QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
Surge: `${name}=ss,${server},${port},encrypt-method=${cipher},password=${password},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'ss',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
cipher,
|
||||
password,
|
||||
plugin: 'obfs',
|
||||
'plugin-opts': {
|
||||
mode: 'http',
|
||||
path: obfs_path,
|
||||
host: obfs_host,
|
||||
},
|
||||
},
|
||||
},
|
||||
V2RAY_PLUGIN_WS: {
|
||||
input: {
|
||||
QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'ss',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
cipher,
|
||||
password,
|
||||
plugin: 'v2ray-plugin',
|
||||
'plugin-opts': {
|
||||
mode: 'websocket',
|
||||
path: obfs_path,
|
||||
host: obfs_host,
|
||||
},
|
||||
},
|
||||
},
|
||||
V2RAY_PLUGIN_WSS: {
|
||||
input: {
|
||||
QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'ss',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
cipher,
|
||||
password,
|
||||
plugin: 'v2ray-plugin',
|
||||
'plugin-opts': {
|
||||
mode: 'websocket',
|
||||
path: obfs_path,
|
||||
host: obfs_host,
|
||||
tls: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const SSR = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
QX: `shadowsocks=${server}:${port},method=${cipher},password=${password},ssr-protocol=${ssr_protocol},ssr-protocol-param=${ssr_protocol_param},obfs=${ssr_obfs},obfs-host=${ssr_obfs_param},tag=${name}`,
|
||||
Loon: `${name}=shadowsocksr,${server},${port},${cipher},"${password}",protocol=${ssr_protocol},protocol-param=${ssr_protocol_param},obfs=${ssr_obfs},obfs-param=${ssr_obfs_param}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'ssr',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
cipher,
|
||||
password,
|
||||
obfs: ssr_obfs,
|
||||
protocol: ssr_protocol,
|
||||
'obfs-param': ssr_obfs_param,
|
||||
'protocol-param': ssr_protocol_param,
|
||||
},
|
||||
},
|
||||
};
|
||||
const TROJAN = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
QX: `trojan=${server}:${port},password=${password},tag=${name}`,
|
||||
Loon: `${name}=trojan,${server},${port},"${password}"`,
|
||||
Surge: `${name}=trojan,${server},${port},password=${password}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'trojan',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
password,
|
||||
},
|
||||
},
|
||||
WS: {
|
||||
input: {
|
||||
QX: `trojan=${server}:${port},password=${password},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
Loon: `${name}=trojan,${server},${port},"${password}",transport=ws,path=${obfs_path},host=${obfs_host}`,
|
||||
Surge: `${name}=trojan,${server},${port},password=${password},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'trojan',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
password,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
WSS: {
|
||||
input: {
|
||||
QX: `trojan=${server}:${port},password=${password},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tls-verification=false,tls-host=${sni},tag=${name}`,
|
||||
Loon: `${name}=trojan,${server},${port},"${password}",transport=ws,path=${obfs_path},host=${obfs_host},over-tls=true,tls-name=${sni},skip-cert-verify=true`,
|
||||
Surge: `${name}=trojan,${server},${port},password=${password},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host},skip-cert-verify=true,sni=${sni},tls=true`,
|
||||
},
|
||||
expected: {
|
||||
type: 'trojan',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
password,
|
||||
network: 'ws',
|
||||
tls: true,
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
},
|
||||
},
|
||||
TLS_FINGERPRINT: {
|
||||
input: {
|
||||
QX: `trojan=${server}:${port},password=${password},tls-verification=false,tls-host=${sni},tls-cert-sha256=${tls_fingerprint},tag=${name},over-tls=true`,
|
||||
Surge: `${name}=trojan,${server},${port},password=${password},skip-cert-verify=true,sni=${sni},tls=true,server-cert-fingerprint-sha256=${tls_fingerprint}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'trojan',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
password,
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
'tls-fingerprint': tls_fingerprint,
|
||||
},
|
||||
},
|
||||
};
|
||||
const VMESS = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
QX: `vmess=${server}:${port},method=${cipher},password=${uuid},tag=${name}`,
|
||||
Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}"`,
|
||||
Surge: `${name}=vmess,${server},${port},username=${uuid}`,
|
||||
},
|
||||
expected: {
|
||||
QX: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
alterId: 0,
|
||||
},
|
||||
Loon: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
alterId: 0,
|
||||
},
|
||||
Surge: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
alterId: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
AEAD: {
|
||||
input: {
|
||||
QX: `vmess=${server}:${port},method=${cipher},password=${uuid},aead=true,tag=${name}`,
|
||||
Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",alterId=0`,
|
||||
Surge: `${name}=vmess,${server},${port},username=${uuid},vmess-aead=true`,
|
||||
},
|
||||
expected: {
|
||||
QX: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
aead: true,
|
||||
alterId: 0,
|
||||
},
|
||||
Loon: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
alterId: 0,
|
||||
},
|
||||
Surge: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
alterId: 0,
|
||||
aead: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
WS: {
|
||||
input: {
|
||||
QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=ws,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path}`,
|
||||
Surge: `${name}=vmess,${server},${port},username=${uuid},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host}`,
|
||||
},
|
||||
expected: {
|
||||
QX: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
alterId: 0,
|
||||
},
|
||||
Loon: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
alterId: 0,
|
||||
},
|
||||
Surge: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
alterId: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
WSS: {
|
||||
input: {
|
||||
QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=wss,obfs-host=${obfs_host},obfs-uri=${obfs_path},tls-verification=false,tls-host=${sni},tag=${name}`,
|
||||
Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,
|
||||
Surge: `${name}=vmess,${server},${port},username=${uuid},ws=true,ws-path=${obfs_path},ws-headers=Host:${obfs_host},skip-cert-verify=true,sni=${sni},tls=true`,
|
||||
},
|
||||
expected: {
|
||||
QX: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
alterId: 0,
|
||||
},
|
||||
Loon: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
alterId: 0,
|
||||
},
|
||||
Surge: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher: 'none', // Surge lacks support for specifying cipher for vmess protocol!
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
alterId: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP: {
|
||||
input: {
|
||||
QX: `vmess=${server}:${port},method=${cipher},password=${uuid},obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path},tag=${name}`,
|
||||
Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
QX: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'http',
|
||||
'http-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
alterId: 0,
|
||||
},
|
||||
Loon: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'http',
|
||||
'http-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
alterId: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP_TLS: {
|
||||
input: {
|
||||
Loon: `${name}=vmess,${server},${port},${cipher},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,
|
||||
},
|
||||
expected: {
|
||||
Loon: {
|
||||
type: 'vmess',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
cipher,
|
||||
network: 'http',
|
||||
'http-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
alterId: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const VLESS = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
Loon: `${name}=vless,${server},${port},"${uuid}"`,
|
||||
},
|
||||
expected: {
|
||||
Loon: {
|
||||
type: 'vless',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
},
|
||||
},
|
||||
},
|
||||
WS: {
|
||||
input: {
|
||||
Loon: `${name}=vless,${server},${port},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
Loon: {
|
||||
type: 'vless',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
WSS: {
|
||||
input: {
|
||||
Loon: `${name}=vless,${server},${port},"${uuid}",transport=ws,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,
|
||||
},
|
||||
expected: {
|
||||
Loon: {
|
||||
type: 'vless',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
network: 'ws',
|
||||
'ws-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP: {
|
||||
input: {
|
||||
Loon: `${name}=vless,${server},${port},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
Loon: {
|
||||
type: 'vless',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
network: 'http',
|
||||
'http-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP_TLS: {
|
||||
input: {
|
||||
Loon: `${name}=vless,${server},${port},"${uuid}",transport=http,host=${obfs_host},path=${obfs_path},over-tls=true,tls-name=${sni},skip-cert-verify=true`,
|
||||
},
|
||||
expected: {
|
||||
Loon: {
|
||||
type: 'vless',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
uuid,
|
||||
network: 'http',
|
||||
'http-opts': {
|
||||
path: obfs_path,
|
||||
headers: {
|
||||
Host: obfs_host,
|
||||
},
|
||||
},
|
||||
tls: true,
|
||||
'skip-cert-verify': true,
|
||||
sni,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const HTTP = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
Loon: `${name}=http,${server},${port}`,
|
||||
QX: `http=${server}:${port},tag=${name}`,
|
||||
Surge: `${name}=http,${server},${port}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'http',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
},
|
||||
},
|
||||
AUTH: {
|
||||
input: {
|
||||
Loon: `${name}=http,${server},${port},${username},"${password}"`,
|
||||
QX: `http=${server}:${port},tag=${name},username=${username},password=${password}`,
|
||||
Surge: `${name}=http,${server},${port},${username},${password}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'http',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
},
|
||||
TLS: {
|
||||
input: {
|
||||
Loon: `${name}=https,${server},${port},${username},"${password}",tls-name=${sni},skip-cert-verify=true`,
|
||||
QX: `http=${server}:${port},username=${username},password=${password},over-tls=true,tls-host=${sni},tls-verification=false,tag=${name}`,
|
||||
Surge: `${name}=https,${server},${port},${username},${password},sni=${sni},skip-cert-verify=true`,
|
||||
},
|
||||
expected: {
|
||||
type: 'http',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
sni,
|
||||
'skip-cert-verify': true,
|
||||
tls: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const SOCKS5 = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
QX: `socks5=${server}:${port},tag=${name}`,
|
||||
Surge: `${name}=socks5,${server},${port}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'socks5',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
},
|
||||
},
|
||||
AUTH: {
|
||||
input: {
|
||||
QX: `socks5=${server}:${port},tag=${name},username=${username},password=${password}`,
|
||||
Surge: `${name}=socks5,${server},${port},${username},${password}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'socks5',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
},
|
||||
TLS: {
|
||||
input: {
|
||||
QX: `socks5=${server}:${port},username=${username},password=${password},over-tls=true,tls-host=${sni},tls-verification=false,tag=${name}`,
|
||||
Surge: `${name}=socks5-tls,${server},${port},${username},${password},sni=${sni},skip-cert-verify=true`,
|
||||
},
|
||||
expected: {
|
||||
type: 'socks5',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
sni,
|
||||
'skip-cert-verify': true,
|
||||
tls: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const SNELL = {
|
||||
SIMPLE: {
|
||||
input: {
|
||||
Surge: `${name}=snell,${server},${port},psk=${password},version=3`,
|
||||
},
|
||||
expected: {
|
||||
type: 'snell',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
psk: password,
|
||||
version: 3,
|
||||
},
|
||||
},
|
||||
OBFS_HTTP: {
|
||||
input: {
|
||||
Surge: `${name}=snell,${server},${port},psk=${password},version=3,obfs=http,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'snell',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
psk: password,
|
||||
version: 3,
|
||||
'obfs-opts': {
|
||||
mode: 'http',
|
||||
host: obfs_host,
|
||||
path: obfs_path,
|
||||
},
|
||||
},
|
||||
},
|
||||
OBFS_TLS: {
|
||||
input: {
|
||||
Surge: `${name}=snell,${server},${port},psk=${password},version=3,obfs=tls,obfs-host=${obfs_host},obfs-uri=${obfs_path}`,
|
||||
},
|
||||
expected: {
|
||||
type: 'snell',
|
||||
name,
|
||||
server,
|
||||
port,
|
||||
psk: password,
|
||||
version: 3,
|
||||
'obfs-opts': {
|
||||
mode: 'tls',
|
||||
host: obfs_host,
|
||||
path: obfs_path,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
SS,
|
||||
SSR,
|
||||
VMESS,
|
||||
VLESS,
|
||||
TROJAN,
|
||||
HTTP,
|
||||
SOCKS5,
|
||||
SNELL,
|
||||
};
|
||||
}
|
||||
|
||||
export default createTestCases();
|
||||
17
backend/src/utils/database.js
Normal file
17
backend/src/utils/database.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export function findByName(list, name, field = 'name') {
|
||||
return list.find((item) => item[field] === name);
|
||||
}
|
||||
|
||||
export function findIndexByName(list, name, field = 'name') {
|
||||
return list.findIndex((item) => item[field] === name);
|
||||
}
|
||||
|
||||
export function deleteByName(list, name, field = 'name') {
|
||||
const idx = findIndexByName(list, name, field);
|
||||
list.splice(idx, 1);
|
||||
}
|
||||
|
||||
export function updateByName(list, name, newItem, field = 'name') {
|
||||
const idx = findIndexByName(list, name, field);
|
||||
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));
|
||||
}
|
||||
346
backend/src/utils/download.js
Normal file
346
backend/src/utils/download.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { hex_md5 } from '@/vendor/md5';
|
||||
import { getPolicyDescriptor } from '@/utils';
|
||||
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 { findByName } from '@/utils/database';
|
||||
import { produceArtifact } from '@/restful/sync';
|
||||
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
|
||||
import { ProxyUtils } from '@/core/proxy-utils';
|
||||
|
||||
const clashPreprocessor = PROXY_PREPROCESSORS.find(
|
||||
(processor) => processor.name === 'Clash Pre-processor',
|
||||
);
|
||||
|
||||
const tasks = new Map();
|
||||
|
||||
export default async function download(
|
||||
rawUrl = '',
|
||||
ua,
|
||||
timeout,
|
||||
customProxy,
|
||||
skipCustomCache,
|
||||
awaitCustomCache,
|
||||
noCache,
|
||||
preprocess,
|
||||
) {
|
||||
let $arguments = {};
|
||||
let url = rawUrl.replace(/#noFlow$/, '');
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
if ($arguments?.cacheKey === true) {
|
||||
$.error(`使用自定义缓存时 cacheKey 的值不能为空`);
|
||||
$arguments.cacheKey = undefined;
|
||||
}
|
||||
|
||||
const customCacheKey = $arguments?.cacheKey
|
||||
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
|
||||
: undefined;
|
||||
|
||||
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
|
||||
.split('#')[0]
|
||||
.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 (type === 'module') {
|
||||
return item.content;
|
||||
} else {
|
||||
return await produceArtifact({
|
||||
type: 'file',
|
||||
name,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when loading ${type}: ${
|
||||
url.split('#')[0]
|
||||
}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法加载 ${type}: ${url}`);
|
||||
}
|
||||
} else if (url?.startsWith('/')) {
|
||||
try {
|
||||
const fs = eval(`require("fs")`);
|
||||
return fs.readFileSync(url.split('#')[0], 'utf8');
|
||||
} catch (err) {
|
||||
$.error(
|
||||
`Error when reading local file: ${
|
||||
url.split('#')[0]
|
||||
}.\n Reason: ${err}`,
|
||||
);
|
||||
throw new Error(`无法从该路径读取文本内容: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNode && tasks.has(id)) {
|
||||
return tasks.get(id);
|
||||
}
|
||||
|
||||
const http = HTTP({
|
||||
headers: {
|
||||
'User-Agent': userAgent,
|
||||
...(isStash && proxy
|
||||
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
|
||||
: {}),
|
||||
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
|
||||
},
|
||||
timeout: requestTimeout,
|
||||
});
|
||||
|
||||
let result;
|
||||
|
||||
// try to find in app cache
|
||||
const cached = resourceCache.get(id);
|
||||
if (!noCache && !$arguments?.noCache && cached) {
|
||||
$.info(`使用缓存: ${url}, ${userAgent}`);
|
||||
result = cached;
|
||||
if (customCacheKey) {
|
||||
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
|
||||
$.write(cached, customCacheKey);
|
||||
}
|
||||
} 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 (preprocess) {
|
||||
try {
|
||||
const proxies = ProxyUtils.parse(body);
|
||||
if (!Array.isArray(proxies) || proxies.length === 0) {
|
||||
$.error(`URL ${url} 不包含有效节点, 不缓存`);
|
||||
shouldCache = false;
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`URL ${url} 尝试解析节点失败 ${e.message ?? e}, 不缓存`,
|
||||
);
|
||||
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) {
|
||||
tasks.set(id, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function downloadFile(url, file) {
|
||||
const undici = eval("require('undici')");
|
||||
const fs = eval("require('fs')");
|
||||
const { pipeline } = eval("require('stream/promises')");
|
||||
const { Agent, interceptors, request } = undici;
|
||||
$.info(`Downloading file...\nURL: ${url}\nFile: ${file}`);
|
||||
const { body, statusCode } = await request(url, {
|
||||
dispatcher: new Agent().compose(
|
||||
interceptors.redirect({
|
||||
maxRedirections: 3,
|
||||
throwOnRedirect: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
if (statusCode !== 200)
|
||||
throw new Error(`Failed to download file from ${url}`);
|
||||
const fileStream = fs.createWriteStream(file);
|
||||
await pipeline(body, fileStream);
|
||||
$.info(`File downloaded from ${url} to ${file}`);
|
||||
return file;
|
||||
}
|
||||
69
backend/src/utils/env.js
Normal file
69
backend/src/utils/env.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { version as substoreVersion } from '../../package.json';
|
||||
import { ENV } from '@/vendor/open-api';
|
||||
|
||||
const {
|
||||
isNode,
|
||||
isQX,
|
||||
isLoon,
|
||||
isSurge,
|
||||
isStash,
|
||||
isShadowRocket,
|
||||
isLanceX,
|
||||
isEgern,
|
||||
isGUIforCores,
|
||||
} = ENV();
|
||||
let backend = 'Node';
|
||||
if (isNode) backend = 'Node';
|
||||
if (isQX) backend = 'QX';
|
||||
if (isLoon) backend = 'Loon';
|
||||
if (isSurge) backend = 'Surge';
|
||||
if (isStash) backend = 'Stash';
|
||||
if (isShadowRocket) backend = 'ShadowRocket';
|
||||
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 {
|
||||
backend,
|
||||
version: substoreVersion,
|
||||
feature,
|
||||
meta,
|
||||
};
|
||||
374
backend/src/utils/flow.js
Normal file
374
backend/src/utils/flow.js
Normal file
@@ -0,0 +1,374 @@
|
||||
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 function getFlowField(headers) {
|
||||
const keys = Object.keys(headers);
|
||||
let sub = '';
|
||||
let webPage = '';
|
||||
for (let k of keys) {
|
||||
const lower = k.toLowerCase();
|
||||
if (lower === 'subscription-userinfo') {
|
||||
sub = headers[k];
|
||||
} else if (lower === 'profile-web-page-url') {
|
||||
webPage = headers[k];
|
||||
}
|
||||
}
|
||||
|
||||
return `${sub || ''}${
|
||||
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 || !/^https?/.test(url)) {
|
||||
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) {
|
||||
if (!flowHeaders) return;
|
||||
// unit is KB
|
||||
const uploadMatch = flowHeaders.match(
|
||||
/upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const upload = Number(uploadMatch[1] + uploadMatch[2]);
|
||||
|
||||
const downloadMatch = flowHeaders.match(
|
||||
/download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const download = Number(downloadMatch[1] + downloadMatch[2]);
|
||||
const totalMatch = flowHeaders.match(
|
||||
/total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const total = Number(totalMatch[1] + totalMatch[2]);
|
||||
|
||||
// optional expire timestamp
|
||||
const expireMatch = flowHeaders.match(
|
||||
/expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
|
||||
);
|
||||
const expires = expireMatch
|
||||
? Number(expireMatch[1] + expireMatch[2])
|
||||
: undefined;
|
||||
|
||||
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') {
|
||||
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
let unitIndex = unitList.indexOf(unit);
|
||||
|
||||
return flow < 1024 || unitIndex === unitList.length - 1
|
||||
? { value: (Math.round(flow * 100) / 100).toString(), unit: unit }
|
||||
: 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
|
||||
let decodedValue = decodeURIComponent(encodedValue);
|
||||
if (
|
||||
['upload', 'download', 'total', 'expire'].includes(
|
||||
key,
|
||||
)
|
||||
) {
|
||||
try {
|
||||
decodedValue = Number(decodedValue).toFixed(0);
|
||||
if (
|
||||
['expire'].includes(key) &&
|
||||
decodedValue <= 0
|
||||
) {
|
||||
decodedValue = '';
|
||||
}
|
||||
} catch (e) {
|
||||
$.error(
|
||||
`Failed to convert value for key "${key}=${encodedValue}": ${
|
||||
e.message ?? e
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
522
backend/src/utils/geo.js
Normal file
522
backend/src/utils/geo.js
Normal file
@@ -0,0 +1,522 @@
|
||||
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'],
|
||||
'🇮🇶': ['IQ', 'IRQ'], // 伊拉克
|
||||
'🇯🇴': ['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'],
|
||||
'🇹🇬': ['TG', 'TGO'], // 多哥
|
||||
'🇹🇭': ['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
|
||||
export function getFlag(name) {
|
||||
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
|
||||
// flags from @surgioproject: https://github.com/surgioproject/surgio/blob/master/lib/misc/flag_cn.ts
|
||||
|
||||
// refer: https://zh.wikipedia.org/wiki/ISO_3166-1二位字母代码
|
||||
// refer: https://zh.wikipedia.org/wiki/ISO_3166-1三位字母代码
|
||||
const Flags = {
|
||||
'🏳️🌈': ['流量', '时间', '过期', 'Bandwidth', 'Expire'],
|
||||
'🇸🇱': ['应急', '测试节点'],
|
||||
'🇦🇩': ['Andorra', '安道尔'],
|
||||
'🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜'],
|
||||
'🇦🇫': ['Afghanistan', '阿富汗'],
|
||||
'🇦🇱': ['Albania', '阿尔巴尼亚', '阿爾巴尼亞'],
|
||||
'🇦🇲': ['Armenia', '亚美尼亚'],
|
||||
'🇦🇷': ['Argentina', '阿根廷'],
|
||||
'🇦🇹': ['Austria', '奥地利', '奧地利', '维也纳'],
|
||||
'🇦🇺': [
|
||||
'Australia',
|
||||
'澳大利亚',
|
||||
'澳洲',
|
||||
'墨尔本',
|
||||
'悉尼',
|
||||
'土澳',
|
||||
'京澳',
|
||||
'廣澳',
|
||||
'滬澳',
|
||||
'沪澳',
|
||||
'广澳',
|
||||
'Sydney',
|
||||
],
|
||||
'🇦🇿': ['Azerbaijan', '阿塞拜疆'],
|
||||
'🇧🇦': ['Bosnia and Herzegovina', '波黑共和国', '波黑'],
|
||||
'🇧🇩': ['Bangladesh', '孟加拉国', '孟加拉'],
|
||||
'🇧🇪': ['Belgium', '比利时', '比利時'],
|
||||
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
|
||||
'🇧🇭': ['Bahrain', '巴林'],
|
||||
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
|
||||
'🇧🇳': ['Brunei', '文莱', '汶萊'],
|
||||
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
|
||||
'🇧🇴': ['Bolivia', '玻利维亚'],
|
||||
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
|
||||
'🇨🇦': [
|
||||
'Canada',
|
||||
'加拿大',
|
||||
'蒙特利尔',
|
||||
'温哥华',
|
||||
'楓葉',
|
||||
'枫叶',
|
||||
'滑铁卢',
|
||||
'多伦多',
|
||||
'Waterloo',
|
||||
'Toronto',
|
||||
],
|
||||
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
|
||||
'🇨🇱': ['Chile', '智利'],
|
||||
'🇨🇴': ['Colombia', '哥伦比亚'],
|
||||
'🇨🇷': ['Costa Rica', '哥斯达黎加'],
|
||||
'🇨🇾': ['Cyprus', '塞浦路斯'],
|
||||
'🇨🇿': ['Czechia', '捷克'],
|
||||
'🇩🇪': [
|
||||
'German',
|
||||
'德国',
|
||||
'德國',
|
||||
'京德',
|
||||
'滬德',
|
||||
'廣德',
|
||||
'沪德',
|
||||
'广德',
|
||||
'法兰克福',
|
||||
'Frankfurt',
|
||||
'德意志',
|
||||
],
|
||||
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
|
||||
'🇪🇨': ['Ecuador', '厄瓜多尔'],
|
||||
'🇪🇪': ['Estonia', '爱沙尼亚'],
|
||||
'🇪🇬': ['Egypt', '埃及'],
|
||||
'🇪🇸': ['Spain', '西班牙'],
|
||||
'🇪🇺': ['European Union', '欧盟', '欧罗巴'],
|
||||
'🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],
|
||||
'🇫🇷': ['France', '法国', '法國', '巴黎'],
|
||||
'🇬🇧': [
|
||||
'Great Britain',
|
||||
'英国',
|
||||
'England',
|
||||
'United Kingdom',
|
||||
'伦敦',
|
||||
'英',
|
||||
'London',
|
||||
],
|
||||
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
|
||||
'🇬🇷': ['Greece', '希腊', '希臘'],
|
||||
'🇬🇺': ['Guam', '关岛', '關島'],
|
||||
'🇬🇹': ['Guatemala', '危地马拉'],
|
||||
'🇭🇰': [
|
||||
'Hongkong',
|
||||
'香港',
|
||||
'Hong Kong',
|
||||
'HongKong',
|
||||
'HONG KONG',
|
||||
'深港',
|
||||
'沪港',
|
||||
'呼港',
|
||||
'穗港',
|
||||
'京港',
|
||||
'港',
|
||||
],
|
||||
'🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'],
|
||||
'🇭🇺': ['Hungary', '匈牙利'],
|
||||
'🇮🇶': ['Iraq', '伊拉克', '巴格达', 'Baghdad'], // 伊拉克
|
||||
'🇯🇴': ['Jordan', '约旦'],
|
||||
'🇯🇵': [
|
||||
'Japan',
|
||||
'日本',
|
||||
'东京',
|
||||
'大阪',
|
||||
'埼玉',
|
||||
'沪日',
|
||||
'穗日',
|
||||
'川日',
|
||||
'中日',
|
||||
'泉日',
|
||||
'杭日',
|
||||
'深日',
|
||||
'辽日',
|
||||
'广日',
|
||||
'大坂',
|
||||
'Osaka',
|
||||
'Tokyo',
|
||||
],
|
||||
'🇰🇪': ['Kenya', '肯尼亚'],
|
||||
'🇰🇬': ['Kyrgyzstan', '吉尔吉斯斯坦'],
|
||||
'🇰🇭': ['Cambodia', '柬埔寨'],
|
||||
'🇰🇵': ['North Korea', '朝鲜'],
|
||||
'🇰🇷': [
|
||||
'Korea',
|
||||
'韩国',
|
||||
'韓國',
|
||||
'韩',
|
||||
'韓',
|
||||
'首尔',
|
||||
'春川',
|
||||
'Chuncheon',
|
||||
'Seoul',
|
||||
],
|
||||
'🇰🇿': ['Kazakhstan', '哈萨克斯坦', '哈萨克'],
|
||||
'🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],
|
||||
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
|
||||
'🇮🇱': ['Israel', '以色列'],
|
||||
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
|
||||
'🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],
|
||||
'🇮🇷': ['Iran', '伊朗'],
|
||||
'🇮🇸': ['Iceland', '冰岛', '冰島'],
|
||||
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
|
||||
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
|
||||
'🇱🇦': ['Laos', '老挝', '老撾'],
|
||||
'🇱🇹': ['Lithuania', '立陶宛'],
|
||||
'🇱🇺': ['Luxembourg', '卢森堡'],
|
||||
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
|
||||
'🇲🇦': ['Morocco', '摩洛哥'],
|
||||
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
|
||||
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
|
||||
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
|
||||
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
|
||||
'🇲🇳': ['Mongolia', '蒙古'],
|
||||
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
|
||||
'🇲🇹': ['Malta', '马耳他'],
|
||||
'🇲🇽': ['Mexico', '墨西哥'],
|
||||
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
|
||||
'🇳🇱': [
|
||||
'Netherlands',
|
||||
'荷兰',
|
||||
'荷蘭',
|
||||
'尼德蘭',
|
||||
'阿姆斯特丹',
|
||||
'Amsterdam',
|
||||
],
|
||||
'🇳🇴': ['Norway', '挪威'],
|
||||
'🇳🇵': ['Nepal', '尼泊尔'],
|
||||
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
|
||||
'🇵🇦': ['Panama', '巴拿马'],
|
||||
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
|
||||
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
|
||||
'🇵🇰': ['Pakistan', '巴基斯坦'],
|
||||
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
|
||||
'🇵🇷': ['Puerto Rico', '波多黎各'],
|
||||
'🇵🇹': ['Portugal', '葡萄牙'],
|
||||
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
|
||||
'🇵🇾': ['Paraguay', '巴拉圭'],
|
||||
'🇷🇴': ['Romania', '罗马尼亚'],
|
||||
'🇷🇸': ['Serbia', '塞尔维亚'],
|
||||
'🇷🇪': ['Réunion', '留尼汪', '法属留尼汪'],
|
||||
'🇷🇺': [
|
||||
'Russia',
|
||||
'俄罗斯',
|
||||
'俄国',
|
||||
'俄羅斯',
|
||||
'伯力',
|
||||
'莫斯科',
|
||||
'圣彼得堡',
|
||||
'西伯利亚',
|
||||
'京俄',
|
||||
'杭俄',
|
||||
'廣俄',
|
||||
'滬俄',
|
||||
'广俄',
|
||||
'沪俄',
|
||||
'Moscow',
|
||||
],
|
||||
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
|
||||
'🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],
|
||||
'🇸🇬': [
|
||||
'Singapore',
|
||||
'新加坡',
|
||||
'狮城',
|
||||
'沪新',
|
||||
'京新',
|
||||
'中新',
|
||||
'泉新',
|
||||
'穗新',
|
||||
'深新',
|
||||
'杭新',
|
||||
'广新',
|
||||
'廣新',
|
||||
'滬新',
|
||||
],
|
||||
'🇸🇮': ['Slovenia', '斯洛文尼亚'],
|
||||
'🇸🇰': ['Slovakia', '斯洛伐克'],
|
||||
'🇹🇬': ['Togo', '多哥', '洛美', 'Lomé', 'Lome'], // 多哥
|
||||
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
|
||||
'🇹🇳': ['Tunisia', '突尼斯'],
|
||||
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],
|
||||
'🇹🇼': [
|
||||
'Taiwan',
|
||||
'台湾',
|
||||
'臺灣',
|
||||
'台灣',
|
||||
'中華民國',
|
||||
'中华民国',
|
||||
'台北',
|
||||
'台中',
|
||||
'新北',
|
||||
'彰化',
|
||||
'台',
|
||||
'臺',
|
||||
'Taipei',
|
||||
'Tai Wan',
|
||||
],
|
||||
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
|
||||
'🇺🇸': [
|
||||
'United States',
|
||||
'美国',
|
||||
'America',
|
||||
'美',
|
||||
'京美',
|
||||
'波特兰',
|
||||
'达拉斯',
|
||||
'俄勒冈',
|
||||
'Oregon',
|
||||
'凤凰城',
|
||||
'费利蒙',
|
||||
'硅谷',
|
||||
'矽谷',
|
||||
'拉斯维加斯',
|
||||
'洛杉矶',
|
||||
'圣何塞',
|
||||
'圣克拉拉',
|
||||
'西雅图',
|
||||
'芝加哥',
|
||||
'沪美',
|
||||
'哥伦布',
|
||||
'纽约',
|
||||
'New York',
|
||||
'Los Angeles',
|
||||
'San Jose',
|
||||
'Sillicon Valley',
|
||||
'Michigan',
|
||||
'俄亥俄',
|
||||
'Ohio',
|
||||
'马纳萨斯',
|
||||
'Manassas',
|
||||
'弗吉尼亚',
|
||||
'Virginia',
|
||||
],
|
||||
'🇺🇾': ['Uruguay', '乌拉圭'],
|
||||
'🇻🇪': ['Venezuela', '委内瑞拉'],
|
||||
'🇻🇳': ['Vietnam', '越南', '胡志明'],
|
||||
'🇿🇦': ['South Africa', '南非'],
|
||||
'🇨🇳': [
|
||||
'China',
|
||||
'中国',
|
||||
'中國',
|
||||
'回国',
|
||||
'回國',
|
||||
'国内',
|
||||
'國內',
|
||||
'华东',
|
||||
'华西',
|
||||
'华南',
|
||||
'华北',
|
||||
'华中',
|
||||
'江苏',
|
||||
'北京',
|
||||
'上海',
|
||||
'广州',
|
||||
'深圳',
|
||||
'杭州',
|
||||
'徐州',
|
||||
'青岛',
|
||||
'宁波',
|
||||
'镇江',
|
||||
],
|
||||
};
|
||||
|
||||
// 原旗帜或空
|
||||
let Flag =
|
||||
name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
|
||||
'🏴☠️';
|
||||
//console.log(`oldFlag = ${Flag}`)
|
||||
// 旗帜匹配
|
||||
for (let flag of Object.keys(Flags)) {
|
||||
const keywords = Flags[flag];
|
||||
//console.log(`keywords = ${keywords}`)
|
||||
if (
|
||||
// 不精确匹配(只要包含就算,忽略大小写)
|
||||
keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
|
||||
) {
|
||||
if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {
|
||||
return (Flag = '🇨🇳');
|
||||
}
|
||||
return (Flag = flag);
|
||||
}
|
||||
}
|
||||
// ISO旗帜匹配
|
||||
for (let flag of Object.keys(ISOFlags)) {
|
||||
const keywords = ISOFlags[flag];
|
||||
//console.log(`keywords = ${keywords}`)
|
||||
if (
|
||||
// 精确匹配(两侧均有分割)
|
||||
keywords.some((keyword) =>
|
||||
RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
|
||||
)
|
||||
) {
|
||||
const isCN2 =
|
||||
flag == '🇨🇳' &&
|
||||
RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);
|
||||
if (!isCN2) {
|
||||
return (Flag = flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//console.log(`Final Flag = ${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;
|
||||
}
|
||||
}
|
||||
292
backend/src/utils/gist.js
Normal file
292
backend/src/utils/gist.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import { HTTP, ENV } from '@/vendor/open-api';
|
||||
import { getPolicyDescriptor } from '@/utils';
|
||||
import $ from '@/core/app';
|
||||
import { SETTINGS_KEY } from '@/constants';
|
||||
|
||||
/**
|
||||
* Gist backup
|
||||
*/
|
||||
export default class Gist {
|
||||
constructor({ token, key, syncPlatform }) {
|
||||
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||
const {
|
||||
defaultProxy,
|
||||
defaultTimeout: timeout,
|
||||
githubProxy,
|
||||
} = $.read(SETTINGS_KEY);
|
||||
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}`,
|
||||
'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: `${
|
||||
githubProxy ? `${githubProxy}/` : ''
|
||||
}https://api.github.com`,
|
||||
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))) {
|
||||
return Promise.reject(
|
||||
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||
);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
this.syncPlatform = syncPlatform;
|
||||
}
|
||||
|
||||
async locate() {
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
return this.http.get('/snippets').then((response) => {
|
||||
const gists = JSON.parse(response.body);
|
||||
|
||||
for (let g of gists) {
|
||||
if (g.title === this.key) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
} 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(input) {
|
||||
if (Object.keys(input).length === 0) {
|
||||
return Promise.reject('未提供需上传的文件');
|
||||
}
|
||||
|
||||
const gist = await this.locate();
|
||||
|
||||
let files = input;
|
||||
|
||||
if (gist?.id) {
|
||||
if (this.syncPlatform === 'gitlab') {
|
||||
gist.files = gist.files.reduce((acc, item) => {
|
||||
acc[item.path] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
// 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 {
|
||||
files = Object.entries(files).reduce((acc, [key, file]) => {
|
||||
if (file.content !== null && file.content !== '') {
|
||||
acc[key] = file;
|
||||
}
|
||||
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) {
|
||||
const gist = await this.locate();
|
||||
if (gist?.id) {
|
||||
try {
|
||||
const { files } = await this.http
|
||||
.get(`/gists/${gist.id}`)
|
||||
.then((resp) => JSON.parse(resp.body));
|
||||
const url = files[filename].raw_url;
|
||||
return await this.http.get(url).then((resp) => resp.body);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
168
backend/src/utils/index.js
Normal file
168
backend/src/utils/index.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as ipAddress from 'ip-address';
|
||||
// source: https://stackoverflow.com/a/36760050
|
||||
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
|
||||
|
||||
// source: https://ihateregex.io/expr/ipv6/
|
||||
const IPV6_REGEX =
|
||||
/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
||||
|
||||
function isIPv4(ip) {
|
||||
return IPV4_REGEX.test(ip);
|
||||
}
|
||||
|
||||
function isIPv6(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) {
|
||||
return typeof str === 'string' && str.trim().length > 0;
|
||||
}
|
||||
|
||||
function getIfNotBlank(str, defaultValue) {
|
||||
return isNotBlank(str) ? str : defaultValue;
|
||||
}
|
||||
|
||||
function isPresent(obj) {
|
||||
return typeof obj !== 'undefined' && obj !== null;
|
||||
}
|
||||
|
||||
function getIfPresent(obj, defaultValue) {
|
||||
return isPresent(obj) ? obj : defaultValue;
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
|
||||
if (isNaN(d.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const replacements = {
|
||||
YYYY: d.getFullYear(),
|
||||
MM: pad(d.getMonth() + 1),
|
||||
DD: pad(d.getDate()),
|
||||
HH: pad(d.getHours()),
|
||||
mm: pad(d.getMinutes()),
|
||||
ss: pad(d.getSeconds()),
|
||||
};
|
||||
|
||||
return format.replace(
|
||||
/YYYY|MM|DD|HH|mm|ss/g,
|
||||
(match) => replacements[match],
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
formatDateTime,
|
||||
isValidUUID,
|
||||
ipAddress,
|
||||
isIPv4,
|
||||
isIPv6,
|
||||
isValidPortNumber,
|
||||
isNotBlank,
|
||||
getIfNotBlank,
|
||||
isPresent,
|
||||
getIfPresent,
|
||||
// utf8ArrayToStr,
|
||||
getPolicyDescriptor,
|
||||
getRandomPort,
|
||||
numberToString,
|
||||
};
|
||||
17
backend/src/utils/logical.js
Normal file
17
backend/src/utils/logical.js
Normal file
@@ -0,0 +1,17 @@
|
||||
function AND(...args) {
|
||||
return args.reduce((a, b) => a.map((c, i) => b[i] && c));
|
||||
}
|
||||
|
||||
function OR(...args) {
|
||||
return args.reduce((a, b) => a.map((c, i) => b[i] || c));
|
||||
}
|
||||
|
||||
function NOT(array) {
|
||||
return array.map((c) => !c);
|
||||
}
|
||||
|
||||
function FULL(length, bool) {
|
||||
return [...Array(length).keys()].map(() => bool);
|
||||
}
|
||||
|
||||
export { AND, OR, NOT, FULL };
|
||||
140
backend/src/utils/migration.js
Normal file
140
backend/src/utils/migration.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
SUBS_KEY,
|
||||
COLLECTIONS_KEY,
|
||||
SCHEMA_VERSION_KEY,
|
||||
ARTIFACTS_KEY,
|
||||
RULES_KEY,
|
||||
FILES_KEY,
|
||||
TOKENS_KEY,
|
||||
} from '@/constants';
|
||||
import $ from '@/core/app';
|
||||
|
||||
export default function migrate() {
|
||||
migrateV2();
|
||||
}
|
||||
|
||||
function migrateV2() {
|
||||
const version = $.read(SCHEMA_VERSION_KEY);
|
||||
if (!version) doMigrationV2();
|
||||
|
||||
// write the current version
|
||||
if (version !== '2.0') {
|
||||
$.write('2.0', SCHEMA_VERSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
function doMigrationV2() {
|
||||
$.info('Start migrating...');
|
||||
// 1. migrate subscriptions
|
||||
const subs = $.read(SUBS_KEY) || {};
|
||||
const newSubs = Object.values(subs).map((sub) => {
|
||||
// set default source to remote
|
||||
sub.source = sub.source || 'remote';
|
||||
|
||||
migrateDisplayName(sub);
|
||||
migrateProcesses(sub);
|
||||
return sub;
|
||||
});
|
||||
$.write(newSubs, SUBS_KEY);
|
||||
|
||||
// 2. migrate collections
|
||||
const collections = $.read(COLLECTIONS_KEY) || {};
|
||||
const newCollections = Object.values(collections).map((collection) => {
|
||||
delete collection.ua;
|
||||
migrateDisplayName(collection);
|
||||
migrateProcesses(collection);
|
||||
return collection;
|
||||
});
|
||||
$.write(newCollections, COLLECTIONS_KEY);
|
||||
|
||||
// 3. migrate artifacts
|
||||
const artifacts = $.read(ARTIFACTS_KEY) || {};
|
||||
const newArtifacts = Object.values(artifacts);
|
||||
$.write(newArtifacts, ARTIFACTS_KEY);
|
||||
|
||||
// 4. migrate rules
|
||||
const rules = $.read(RULES_KEY) || {};
|
||||
const newRules = Object.values(rules);
|
||||
$.write(newRules, RULES_KEY);
|
||||
|
||||
// 5. migrate files
|
||||
const files = $.read(FILES_KEY) || {};
|
||||
const newFiles = Object.values(files);
|
||||
$.write(newFiles, FILES_KEY);
|
||||
|
||||
// 6. migrate tokens
|
||||
const tokens = $.read(TOKENS_KEY) || {};
|
||||
const newTokens = Object.values(tokens);
|
||||
$.write(newTokens, TOKENS_KEY);
|
||||
|
||||
// 7. delete builtin rules
|
||||
delete $.cache.builtin;
|
||||
$.info('Migration complete!');
|
||||
|
||||
function migrateDisplayName(item) {
|
||||
const displayName = item['display-name'];
|
||||
if (displayName) {
|
||||
item.displayName = displayName;
|
||||
delete item['display-name'];
|
||||
}
|
||||
}
|
||||
|
||||
function migrateProcesses(item) {
|
||||
const processes = item.process;
|
||||
if (!processes || processes.length === 0) return;
|
||||
const newProcesses = [];
|
||||
const quickSettingOperator = {
|
||||
type: 'Quick Setting Operator',
|
||||
args: {
|
||||
udp: 'DEFAULT',
|
||||
tfo: 'DEFAULT',
|
||||
scert: 'DEFAULT',
|
||||
'vmess aead': 'DEFAULT',
|
||||
useless: 'DEFAULT',
|
||||
},
|
||||
};
|
||||
for (const p of processes) {
|
||||
if (!p.type) continue;
|
||||
if (p.type === 'Useless Filter') {
|
||||
quickSettingOperator.args.useless = 'ENABLED';
|
||||
} else if (p.type === 'Set Property Operator') {
|
||||
const { key, value } = p.args;
|
||||
switch (key) {
|
||||
case 'udp':
|
||||
quickSettingOperator.args.udp = value
|
||||
? 'ENABLED'
|
||||
: 'DISABLED';
|
||||
break;
|
||||
case 'tfo':
|
||||
quickSettingOperator.args.tfo = value
|
||||
? 'ENABLED'
|
||||
: 'DISABLED';
|
||||
break;
|
||||
case 'skip-cert-verify':
|
||||
quickSettingOperator.args.scert = value
|
||||
? 'ENABLED'
|
||||
: 'DISABLED';
|
||||
break;
|
||||
case 'aead':
|
||||
quickSettingOperator.args['vmess aead'] = value
|
||||
? 'ENABLED'
|
||||
: 'DISABLED';
|
||||
break;
|
||||
}
|
||||
} else if (p.type.indexOf('Keyword') !== -1) {
|
||||
// drop keyword operators and keyword filters
|
||||
} else if (p.type === 'Flag Operator') {
|
||||
// set default args
|
||||
const add = typeof p.args === 'undefined' ? true : p.args;
|
||||
p.args = {
|
||||
mode: add ? 'add' : 'remove',
|
||||
};
|
||||
newProcesses.push(p);
|
||||
} else {
|
||||
newProcesses.push(p);
|
||||
}
|
||||
}
|
||||
newProcesses.unshift(quickSettingOperator);
|
||||
item.process = newProcesses;
|
||||
}
|
||||
}
|
||||
66
backend/src/utils/resource-cache.js
Normal file
66
backend/src/utils/resource-cache.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import $ from '@/core/app';
|
||||
import { CACHE_EXPIRATION_TIME_MS, RESOURCE_CACHE_KEY } from '@/constants';
|
||||
|
||||
class ResourceCache {
|
||||
constructor(expires) {
|
||||
this.expires = expires;
|
||||
if (!$.read(RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', RESOURCE_CACHE_KEY);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
// clear obsolete cached resource
|
||||
let clear = false;
|
||||
Object.entries(this.resourceCache).forEach((entry) => {
|
||||
const [id, updated] = entry;
|
||||
if (!updated.time) {
|
||||
// clear old version cache
|
||||
delete this.resourceCache[id];
|
||||
$.delete(`#${id}`);
|
||||
clear = true;
|
||||
}
|
||||
if (new Date().getTime() - updated.time > this.expires) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
});
|
||||
if (clear) this._persist();
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
$.write(JSON.stringify(this.resourceCache), RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache(CACHE_EXPIRATION_TIME_MS);
|
||||
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,
|
||||
};
|
||||
124
backend/src/utils/script-resource-cache.js
Normal file
124
backend/src/utils/script-resource-cache.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import $ from '@/core/app';
|
||||
import {
|
||||
SCRIPT_RESOURCE_CACHE_KEY,
|
||||
CSR_EXPIRATION_TIME_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
class ResourceCache {
|
||||
constructor() {
|
||||
this.expires = getExpiredTime();
|
||||
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
|
||||
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
_cleanup(prefix, expires) {
|
||||
// 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 >
|
||||
(expires ?? this.expires) ||
|
||||
(prefix && id.startsWith(prefix))
|
||||
) {
|
||||
delete this.resourceCache[id];
|
||||
clear = true;
|
||||
}
|
||||
});
|
||||
if (clear) this._persist();
|
||||
}
|
||||
|
||||
revokeAll() {
|
||||
this.resourceCache = {};
|
||||
this._persist();
|
||||
}
|
||||
|
||||
_persist() {
|
||||
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
|
||||
}
|
||||
|
||||
get(id, expires, remove) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated) {
|
||||
if (new Date().getTime() - updated <= (expires ?? this.expires))
|
||||
return this.resourceCache[id].data;
|
||||
if (remove) {
|
||||
delete this.resourceCache[id];
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
gettime(id) {
|
||||
const updated = this.resourceCache[id] && this.resourceCache[id].time;
|
||||
if (updated && new Date().getTime() - updated <= this.expires) {
|
||||
return this.resourceCache[id].time;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
this.resourceCache[id] = { time: new Date().getTime(), data: value };
|
||||
this._persist();
|
||||
}
|
||||
}
|
||||
|
||||
function getExpiredTime() {
|
||||
// console.log($.read(CSR_EXPIRATION_TIME_KEY));
|
||||
if (!$.read(CSR_EXPIRATION_TIME_KEY)) {
|
||||
$.write('1728e5', CSR_EXPIRATION_TIME_KEY); // 48 * 3600 * 1000
|
||||
}
|
||||
let expiration = 1728e5;
|
||||
if ($.env.isLoon) {
|
||||
const loont = {
|
||||
// Loon 插件自义定
|
||||
'1\u5206\u949f': 6e4,
|
||||
'5\u5206\u949f': 3e5,
|
||||
'10\u5206\u949f': 6e5,
|
||||
'30\u5206\u949f': 18e5, // "30分钟"
|
||||
'1\u5c0f\u65f6': 36e5,
|
||||
'2\u5c0f\u65f6': 72e5,
|
||||
'3\u5c0f\u65f6': 108e5,
|
||||
'6\u5c0f\u65f6': 216e5,
|
||||
'12\u5c0f\u65f6': 432e5,
|
||||
'24\u5c0f\u65f6': 864e5,
|
||||
'48\u5c0f\u65f6': 1728e5,
|
||||
'72\u5c0f\u65f6': 2592e5, // "72小时"
|
||||
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
|
||||
};
|
||||
let intimed = $.read('#\u8282\u70b9\u7f13\u5b58\u6709\u6548\u671f'); // Loon #节点缓存有效期
|
||||
// console.log(intimed);
|
||||
if (intimed in loont) {
|
||||
expiration = loont[intimed];
|
||||
if (expiration === 'readcachets') {
|
||||
expiration = intimed;
|
||||
}
|
||||
}
|
||||
return expiration;
|
||||
} else {
|
||||
expiration = $.read(CSR_EXPIRATION_TIME_KEY);
|
||||
return expiration;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResourceCache();
|
||||
101
backend/src/utils/user-agent.js
Normal file
101
backend/src/utils/user-agent.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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 || ua.indexOf('singbox') !== -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', 'Loon'].includes(target)) {
|
||||
// return false;
|
||||
// }
|
||||
// const coerceVersion = coerce(ua);
|
||||
// $.log(JSON.stringify(coerceVersion, null, 2));
|
||||
// const { version } = coerceVersion;
|
||||
// if (
|
||||
// platform === 'Stash' &&
|
||||
// target === 'Stash' &&
|
||||
// gte(version, '3.1.0')
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
// if (
|
||||
// platform === 'Egern' &&
|
||||
// target === 'Egern' &&
|
||||
// gte(version, '1.29.0')
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
// // Loon 的 UA 不规范, version 取出来是 build
|
||||
// if (
|
||||
// platform === 'Loon' &&
|
||||
// target === 'Loon' &&
|
||||
// gte(version, '842.0.0')
|
||||
// ) {
|
||||
// return true;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// $.error(`获取版本号失败: ${e}`);
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
39
backend/src/utils/yaml.js
Normal file
39
backend/src/utils/yaml.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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,
|
||||
parse: safeLoad,
|
||||
stringify: safeDump,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user