Compare commits

..

63 Commits

Author SHA1 Message Date
xream
6d43961e96 feat: Node.js 支持使用环境变量 SUB_STORE_BACKEND_DEFAULT_PROXY 设置默认代理; ProxyUtils 增加 download 方法 2024-10-07 18:43:29 +08:00
xream
f3200aea8c feat: 流量和同步配置也使用默认代理/策略 2024-10-07 18:34:39 +08:00
xream
e2346d16a2 feat: 新增全局代理/策略设置, 前端 > 2.14.265 2024-10-07 18:05:06 +08:00
xream
dc320eaa6c feat(file): 新增启用下载(文件名为显示名称), 前端 > 2.14.264 2024-10-07 17:26:15 +08:00
xream
02031019f7 doc: demo.js 2024-09-22 06:02:58 +08:00
xream
5d09fe782f feat: 增加 _subDisplayName _collectionDisplayName 2024-09-18 19:42:53 +08:00
xream
6e425e5908 doc: demo.js 2024-09-18 19:22:12 +08:00
xream
d10c9233c0 feat: 正式弃用旧的 subName 和 collectionName 2024-09-18 19:18:50 +08:00
xream
cc556b641d fix: 修复 password 为数字时的 bug 2024-09-16 01:43:16 +08:00
xream
de2813b035 feat: 使用自定义缓存时 cacheKey 的值不能为空 2024-09-13 23:42:55 +08:00
xream
c9158ceb1d feat: 内置的 Google/Cloudflare DNS 更换为 DoH 2024-09-09 14:56:47 +08:00
xream
5cf0c98f5f chroe: 修改脚本链接为 release 分支 2024-09-07 23:21:42 +08:00
xream
7d0414f8ca fix: 传输层 path 应为以 / 开头的字符串 2024-09-05 17:39:42 +08:00
xream
bee1d62a1a fix: 传输层 path 应以 / 开头 2024-09-05 17:15:30 +08:00
xream
72bc9b9456 feat: 处理非字符串的 ports 字段 2024-09-04 13:40:17 +08:00
xream
3b4c14e7d0 doc: README 2024-09-04 10:49:52 +08:00
xream
59d93483bb feat: Node.js 版支持环境变量 SUB_STORE_BACKEND_DOWNLOAD_CRON 设置定时恢复配置, SUB_STORE_BACKEND_UPLOAD_CRON 设置定时备份配置, SUB_STORE_BACKEND_SYNC_CRON 设置定时同步订阅/文件 2024-09-04 02:20:28 +08:00
xream
75d88c02c7 feat: SurgeMac 支持使用 mihomo 来支援 Surge 本身不支持的协议; 弃用旧的 ssr-local 方案 2024-09-03 20:31:42 +08:00
xream
99d5868cff feat: 订阅和文件的请求链接支持传入 $options , 可在脚本中使用 2024-09-03 13:58:10 +08:00
xream
e1489a3cf7 feat: sing-box VLESS Reality uTLS 默认启用 2024-09-02 21:20:22 +08:00
xream
59fe16a7b0 feat: Surge Hysteria2 与 TUIC 协议支持端口跳跃; Hysteria2 URI 的端口部分支持 端口跳跃 的「多端口地址格式」 2024-09-02 16:38:21 +08:00
xream
562d349629 feat: 脚本操作传入上下文 require (仅对应的环境支持)" 2024-08-31 22:39:54 +08:00
egerndaddy
9ce14351c5 doc: 添加 Egern 模块链接 2024-08-29 13:26:27 +08:00
egerndaddy
76e781c711 Create Egern.yaml 2024-08-29 13:09:59 +08:00
xream
f0acf4a2a7 fix: DoH 结果过滤 2024-08-29 12:30:49 +08:00
xream
9abeb4ce7b fix: 修复 SurgeMac ShadowsocksR obfs-param 2024-08-28 14:51:06 +08:00
xream
153802c7c4 feat: Loon SOCKS5 UDP 2024-08-26 00:33:22 +08:00
xream
19418b631f feat(uri): VMess URI 输入支持 allowInsecure(输出不支持, 与 2dust/v2rayN 分享链接逻辑一致) 2024-08-18 15:53:13 +08:00
xream
97caeed208 feat(geo): 增加 利雅得 Riyadh 2024-08-17 14:06:28 +08:00
xream
dd8d1d85e8 feat: 支持 Loon tls-pubkey-sha256, tls-cert-sha256 2024-07-30 22:17:25 +08:00
xream
14ed56b5d5 chore: 传输层应该有配置, 暂时不考虑兼容不给配置的节点 2024-07-24 11:27:33 +08:00
xream
9785271c5b chore: 增加部分 clash.meta(mihomo) 内核客户端的 User-Agent(clash-verge, flclash) 2024-07-20 14:48:39 +08:00
xream
05bdf95a29 feat: 处理端口跳跃(感谢亚托莉佬) 2024-07-19 15:23:44 +08:00
xream
317a804b36 fix: 修复 URI 报错 2024-07-19 14:33:34 +08:00
xream
10ec8a25a2 feat: 处理不规范的 hysteria2 节点 2024-07-19 09:45:28 +08:00
xream
aa0943a909 fix: 被识别为 IP4P 的域名解析结果均增加 _IP4P 字段; 修复报错 2024-07-18 19:48:01 +08:00
xream
a0c1bbbf70 fix: 域名解析修复; 结果增加 _IP4P 字段 2024-07-18 19:42:57 +08:00
xream
fea9de4fae feat: IP4P 合并进 IPv6; ProxyUtils 中增加 ipAddress 2024-07-18 18:35:22 +08:00
xream
cddd1818fe chore: bump release version 2024-07-08 02:51:21 +08:00
xream
f94830b2df Merge pull request #339 from zhiqiang02/add-tai-wan-keyword
Add 'Tai Wan' as a keyword for Taiwan flag
2024-07-08 02:50:13 +08:00
zhiqiang02
e02a26040e Add 'Tai Wan' as a keyword for Taiwan flag
碰到了某机场奇奇怪怪的节点名
2024-07-08 02:38:50 +08:00
xream
6906efdd55 chore: bump release version 2024-07-02 21:04:14 +08:00
xream
9558b63261 Merge pull request #336 from cooip-jm/patch-1
处理grpc-opts为 {} 的情况
2024-07-02 21:03:30 +08:00
cooip-jm
4bfdef17ee 处理grpc-opts为 {} 的情况
该字段仅影响sing-box内核,对mihomo无影响
2024-07-02 21:01:11 +08:00
xream
9d29fc8a09 feat: 处理 reality-opts 为 {} 的情况 2024-07-02 20:39:04 +08:00
xream
f524920c13 feat: 文件支持设置 查询流量信息订阅链接. 服务器版中使用此链接可在响应中传递订阅流量信息 2024-06-28 18:34:26 +08:00
xream
bfe072cbdf feat: 域名解析支持自定义 EDNS(需新版前端) 2024-06-22 11:45:37 +08:00
xream
32dcca4a26 feat: 域名解析支持自定义 DoH(需新版前端) 2024-06-20 21:42:15 +08:00
xream
a5d77c39c8 feat: 域名解析增加超时参数(默认使用全局超时) 2024-06-20 13:41:05 +08:00
xream
6ea1b69a62 doc: demo.js 增加更多字段的说明 2024-06-20 11:44:07 +08:00
xream
2b3b9177e5 feat: 域名解析新增 _resolved_ips 为解析出的所有 IP 2024-06-20 11:28:17 +08:00
xream
91aab3ca7a fix: 修复 Tencent DNS 缓存 2024-06-20 10:59:06 +08:00
xream
c1a9fc6abc fix: 修复 Loon Hysteria2 salamander 混淆 2024-06-17 11:08:43 +08:00
xream
11d9ce7372 feat: 支持 Loon Hysteria2 salamander 混淆 2024-06-16 21:49:13 +08:00
xream
ad3d2270ac feat: 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint 2024-06-13 20:44:12 +08:00
xream
3ad42f2c10 feat: Stash 支持 juicity, ssh 2024-06-12 15:16:56 +08:00
xream
ec06eb8659 fix: sing-box tls cert 应该为数组 2024-06-10 19:10:57 +08:00
xream
4a23a4d8b6 fix: tlsParser typo 2024-06-10 19:07:19 +08:00
xream
913638a233 feat: /api/sub/flow/:name 接口支持指定远程订阅 url(可携带订阅 url 支持的参数, 例如 flowUserAgent) 2024-06-10 13:24:06 +08:00
xream
bf642ce0e6 fix: 兼容空的订阅链接 2024-06-09 01:42:40 +08:00
xream
1ecac9da92 chore: demo.js 2024-06-06 21:50:13 +08:00
xream
c5a417da8f feat: VMess URI 支持 TCP/H2 传输层 2024-06-03 21:14:07 +08:00
xream
8cd0545023 feat: ws, http, h2 传输层补全 path 2024-06-03 00:34:03 +08:00
47 changed files with 1329 additions and 358 deletions

View File

@@ -10,10 +10,10 @@
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket. Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
</p> </p>
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store) [![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
<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> <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>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
Core functionalities: Core functionalities:
1. Conversion among various formats. 1. Conversion among various formats.
@@ -21,7 +21,7 @@ Core functionalities:
3. Collect multiple subscriptions in one URL. 3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information. > The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion ## 1. Subscription Conversion
### Supported Input Formats ### Supported Input Formats
@@ -31,11 +31,11 @@ Core functionalities:
- [x] Clash Proxy JSON(single line) - [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge)) - [x] Surge (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] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC) - [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC) - [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard) - [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms
@@ -46,6 +46,7 @@ Core functionalities:
- [x] Clash - [x] Clash
- [x] Surfboard - [x] Surfboard
- [x] Surge - [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
- [x] Loon - [x] Loon
- [x] Shadowrocket - [x] Shadowrocket
- [x] QX - [x] QX
@@ -98,7 +99,7 @@ or
esbuild(experimental) esbuild(experimental)
``` ```
pnpm run --parallel "/^dev:.*/" SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
``` ```
## LICENSE ## LICENSE
@@ -111,7 +112,6 @@ This project is under the GPL V3 LICENSE.
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date) [![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements ## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work! - Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.331", "version": "2.14.389",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
@@ -20,11 +20,15 @@
"@maxmind/geoip2-node": "^5.0.0", "@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7", "automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^2.0.0", "connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6", "cron": "^3.1.6",
"dns-packet": "^5.6.1",
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^2.0.6",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"request": "^2.88.2", "request": "^2.88.2",
"requests": "^0.3.0", "requests": "^0.3.0",

66
backend/pnpm-lock.yaml generated
View File

@@ -14,21 +14,33 @@ dependencies:
body-parser: body-parser:
specifier: ^1.19.0 specifier: ^1.19.0
version: registry.npmmirror.com/body-parser@1.19.0 version: registry.npmmirror.com/body-parser@1.19.0
buffer:
specifier: ^6.0.3
version: registry.npmmirror.com/buffer@6.0.3
connect-history-api-fallback: connect-history-api-fallback:
specifier: ^2.0.0 specifier: ^2.0.0
version: registry.npmmirror.com/connect-history-api-fallback@2.0.0 version: registry.npmmirror.com/connect-history-api-fallback@2.0.0
cron: cron:
specifier: ^3.1.6 specifier: ^3.1.6
version: registry.npmmirror.com/cron@3.1.6 version: registry.npmmirror.com/cron@3.1.6
dns-packet:
specifier: ^5.6.1
version: registry.npmmirror.com/dns-packet@5.6.1
express: express:
specifier: ^4.17.1 specifier: ^4.17.1
version: registry.npmmirror.com/express@4.17.1 version: registry.npmmirror.com/express@4.17.1
http-proxy-middleware: http-proxy-middleware:
specifier: ^2.0.6 specifier: ^2.0.6
version: registry.npmmirror.com/http-proxy-middleware@2.0.6 version: registry.npmmirror.com/http-proxy-middleware@2.0.6
ip-address:
specifier: ^9.0.5
version: registry.npmmirror.com/ip-address@9.0.5
js-base64: js-base64:
specifier: ^3.7.2 specifier: ^3.7.2
version: registry.npmmirror.com/js-base64@3.7.2 version: registry.npmmirror.com/js-base64@3.7.2
jsrsasign:
specifier: ^11.1.0
version: registry.npmmirror.com/jsrsasign@11.1.0
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: registry.npmmirror.com/lodash@4.17.21 version: registry.npmmirror.com/lodash@4.17.21
@@ -1970,6 +1982,12 @@ packages:
'@jridgewell/sourcemap-codec': registry.npmmirror.com/@jridgewell/sourcemap-codec@1.4.13 '@jridgewell/sourcemap-codec': registry.npmmirror.com/@jridgewell/sourcemap-codec@1.4.13
dev: true dev: true
registry.npmmirror.com/@leichtgewicht/ip-codec@2.0.5:
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz}
name: '@leichtgewicht/ip-codec'
version: 2.0.5
dev: false
registry.npmmirror.com/@maxmind/geoip2-node@5.0.0: registry.npmmirror.com/@maxmind/geoip2-node@5.0.0:
resolution: {integrity: sha512-ki+q5//oU4tZ3BAhegZJcB5czoZyic5JSTEKbrUAQB/BzAoAiGyLW0immEmQvVVyy2SMlvBTJ3zqyRj8K9BbwQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@maxmind/geoip2-node/-/geoip2-node-5.0.0.tgz} resolution: {integrity: sha512-ki+q5//oU4tZ3BAhegZJcB5czoZyic5JSTEKbrUAQB/BzAoAiGyLW0immEmQvVVyy2SMlvBTJ3zqyRj8K9BbwQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@maxmind/geoip2-node/-/geoip2-node-5.0.0.tgz}
name: '@maxmind/geoip2-node' name: '@maxmind/geoip2-node'
@@ -2704,7 +2722,6 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz}
name: base64-js name: base64-js
version: 1.5.1 version: 1.5.1
dev: true
registry.npmmirror.com/base@0.11.2: registry.npmmirror.com/base@0.11.2:
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base/-/base-0.11.2.tgz} resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/base/-/base-0.11.2.tgz}
@@ -3075,6 +3092,15 @@ packages:
ieee754: registry.npmmirror.com/ieee754@1.2.1 ieee754: registry.npmmirror.com/ieee754@1.2.1
dev: true dev: true
registry.npmmirror.com/buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz}
name: buffer
version: 6.0.3
dependencies:
base64-js: registry.npmmirror.com/base64-js@1.5.1
ieee754: registry.npmmirror.com/ieee754@1.2.1
dev: false
registry.npmmirror.com/builtin-status-codes@3.0.0: registry.npmmirror.com/builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz} resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz}
name: builtin-status-codes name: builtin-status-codes
@@ -4044,6 +4070,15 @@ packages:
randombytes: registry.npmmirror.com/randombytes@2.1.0 randombytes: registry.npmmirror.com/randombytes@2.1.0
dev: true dev: true
registry.npmmirror.com/dns-packet@5.6.1:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dns-packet/-/dns-packet-5.6.1.tgz}
name: dns-packet
version: 5.6.1
engines: {node: '>=6'}
dependencies:
'@leichtgewicht/ip-codec': registry.npmmirror.com/@leichtgewicht/ip-codec@2.0.5
dev: false
registry.npmmirror.com/doctrine@3.0.0: registry.npmmirror.com/doctrine@3.0.0:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz}
name: doctrine name: doctrine
@@ -5871,7 +5906,6 @@ packages:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz}
name: ieee754 name: ieee754
version: 1.2.1 version: 1.2.1
dev: true
registry.npmmirror.com/ignore-by-default@1.0.1: registry.npmmirror.com/ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz} resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz}
@@ -6000,6 +6034,16 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
registry.npmmirror.com/ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ip-address/-/ip-address-9.0.5.tgz}
name: ip-address
version: 9.0.5
engines: {node: '>= 12'}
dependencies:
jsbn: registry.npmmirror.com/jsbn@1.1.0
sprintf-js: registry.npmmirror.com/sprintf-js@1.1.3
dev: false
registry.npmmirror.com/ip6addr@0.2.5: registry.npmmirror.com/ip6addr@0.2.5:
resolution: {integrity: sha512-9RGGSB6Zc9Ox5DpDGFnJdIeF0AsqXzdH+FspCfPPaU/L/4tI6P+5lIoFUFm9JXs9IrJv1boqAaNCQmoDADTSKQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ip6addr/-/ip6addr-0.2.5.tgz} resolution: {integrity: sha512-9RGGSB6Zc9Ox5DpDGFnJdIeF0AsqXzdH+FspCfPPaU/L/4tI6P+5lIoFUFm9JXs9IrJv1boqAaNCQmoDADTSKQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ip6addr/-/ip6addr-0.2.5.tgz}
name: ip6addr name: ip6addr
@@ -6545,6 +6589,12 @@ packages:
version: 0.1.1 version: 0.1.1
dev: false dev: false
registry.npmmirror.com/jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz}
name: jsbn
version: 1.1.0
dev: false
registry.npmmirror.com/jsesc@0.5.0: registry.npmmirror.com/jsesc@0.5.0:
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz} resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz}
name: jsesc name: jsesc
@@ -6634,6 +6684,12 @@ packages:
verror: registry.npmmirror.com/verror@1.10.0 verror: registry.npmmirror.com/verror@1.10.0
dev: false dev: false
registry.npmmirror.com/jsrsasign@11.1.0:
resolution: {integrity: sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsrsasign/-/jsrsasign-11.1.0.tgz}
name: jsrsasign
version: 11.1.0
dev: false
registry.npmmirror.com/just-debounce@1.1.0: registry.npmmirror.com/just-debounce@1.1.0:
resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/just-debounce/-/just-debounce-1.1.0.tgz} resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/just-debounce/-/just-debounce-1.1.0.tgz}
name: just-debounce name: just-debounce
@@ -9058,6 +9114,12 @@ packages:
version: 1.0.3 version: 1.0.3
dev: false dev: false
registry.npmmirror.com/sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz}
name: sprintf-js
version: 1.1.3
dev: false
registry.npmmirror.com/sshpk@1.16.1: registry.npmmirror.com/sshpk@1.16.1:
resolution: {integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sshpk/-/sshpk-1.16.1.tgz} resolution: {integrity: sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sshpk/-/sshpk-1.16.1.tgz}
name: sshpk name: sshpk

View File

@@ -1,3 +1,5 @@
import { Buffer } from 'buffer';
import rs from '@/utils/rs';
import YAML from '@/utils/yaml'; import YAML from '@/utils/yaml';
import download from '@/utils/download'; import download from '@/utils/download';
import { import {
@@ -5,7 +7,9 @@ import {
isIPv6, isIPv6,
isValidPortNumber, isValidPortNumber,
isNotBlank, isNotBlank,
utf8ArrayToStr, ipAddress,
getRandomPort,
numberToString,
} from '@/utils'; } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors'; import PROXY_PREPROCESSORS from './preprocessors';
@@ -74,7 +78,13 @@ function parse(raw) {
return proxies; return proxies;
} }
async function processFn(proxies, operators = [], targetPlatform, source) { async function processFn(
proxies,
operators = [],
targetPlatform,
source,
$options,
) {
for (const item of operators) { for (const item of operators) {
// process script // process script
let script; let script;
@@ -83,7 +93,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
let noCache; let noCache;
let url = content; let url = content || '';
if (url.endsWith('#noCache')) { if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, ''); url = url.replace(/#noCache$/, '');
noCache = true; noCache = true;
@@ -173,6 +183,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
targetPlatform, targetPlatform,
$arguments, $arguments,
source, source,
$options,
); );
} else { } else {
processor = PROXY_PROCESSORS[item.type](item.args || {}); processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -199,8 +210,6 @@ function produce(proxies, targetPlatform, type, opts = {}) {
); );
proxies = proxies.map((proxy) => { proxies = proxies.map((proxy) => {
proxy._subName = proxy.subName;
proxy._collectionName = proxy.collectionName;
proxy._resolved = proxy.resolved; proxy._resolved = proxy.resolved;
if (!isNotBlank(proxy.name)) { if (!isNotBlank(proxy.name)) {
@@ -218,26 +227,27 @@ function produce(proxies, targetPlatform, type, opts = {}) {
delete proxy['tls-fingerprint']; 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; return proxy;
}); });
$.log(`Producing proxies for target: ${targetPlatform}`); $.log(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
let list = proxies let list = proxies
.map((proxy) => { .map((proxy) => {
try { try {
let line = producer.produce(proxy, type, opts); return producer.produce(proxy, type, opts);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
} catch (err) { } catch (err) {
$.error( $.error(
`Cannot produce proxy: ${JSON.stringify( `Cannot produce proxy: ${JSON.stringify(
@@ -256,7 +266,7 @@ function produce(proxies, targetPlatform, type, opts = {}) {
proxies.length > 0 && proxies.length > 0 &&
proxies.every((p) => p.type === 'wireguard') proxies.every((p) => p.type === 'wireguard')
) { ) {
list = `#!name=${proxies[0]?.subName} list = `#!name=${proxies[0]?._subName}
#!desc=${proxies[0]?._desc ?? ''} #!desc=${proxies[0]?._desc ?? ''}
#!category=${proxies[0]?._category ?? ''} #!category=${proxies[0]?._category ?? ''}
${list}`; ${list}`;
@@ -271,6 +281,8 @@ export const ProxyUtils = {
parse, parse,
process: processFn, process: processFn,
produce, produce,
ipAddress,
getRandomPort,
isIPv4, isIPv4,
isIPv6, isIPv6,
isIP, isIP,
@@ -280,6 +292,7 @@ export const ProxyUtils = {
getISO, getISO,
MMDB, MMDB,
Gist, Gist,
download,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -300,7 +313,23 @@ function safeMatch(parser, line) {
} }
} }
function formatTransportPath(path) {
if (typeof path === 'string' || typeof path === 'number') {
path = String(path).trim();
if (path === '') {
return '/';
} else if (!path.startsWith('/')) {
return '/' + path;
}
}
return path;
}
function lastParse(proxy) { function lastParse(proxy) {
if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password);
}
if (proxy.interface) { if (proxy.interface) {
proxy['interface-name'] = proxy.interface; proxy['interface-name'] = proxy.interface;
delete proxy.interface; delete proxy.interface;
@@ -328,6 +357,17 @@ function lastParse(proxy) {
delete proxy['ws-headers']; delete proxy['ws-headers'];
} }
const transportPath = proxy[`${proxy.network}-opts`]?.path;
if (Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = transportPath.map((item) =>
formatTransportPath(item),
);
} else if (transportPath != null) {
proxy[`${proxy.network}-opts`].path =
formatTransportPath(transportPath);
}
if (proxy.type === 'trojan') { if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') { if (proxy.network === 'tcp') {
delete proxy.network; delete proxy.network;
@@ -338,7 +378,11 @@ function lastParse(proxy) {
proxy.network = 'tcp'; proxy.network = 'tcp';
} }
} }
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
proxy.tls = true; proxy.tls = true;
} }
if (proxy.network) { if (proxy.network) {
@@ -404,10 +448,37 @@ function lastParse(proxy) {
proxy[`${proxy.network}-opts`].path = [transportPath]; proxy[`${proxy.network}-opts`].path = [transportPath];
} }
} }
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) { // if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) {
if (proxy.ports) {
proxy.ports = String(proxy.ports).replace(/\//g, ',');
} else {
delete proxy.ports; delete proxy.ports;
} }
// }
if (
['hysteria2'].includes(proxy.type) &&
proxy.obfs &&
!['salamander'].includes(proxy.obfs) &&
!proxy['obfs-password']
) {
proxy['obfs-password'] = proxy.obfs;
proxy.obfs = 'salamander';
}
if (['vless'].includes(proxy.type)) { 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 没有意义 // 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) { if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow; delete proxy.flow;
@@ -422,6 +493,7 @@ function lastParse(proxy) {
} }
} }
} }
if (typeof proxy.name !== 'string') { if (typeof proxy.name !== 'string') {
if (/^\d+$/.test(proxy.name)) { if (/^\d+$/.test(proxy.name)) {
proxy.name = `${proxy.name}`; proxy.name = `${proxy.name}`;
@@ -430,7 +502,7 @@ function lastParse(proxy) {
if (proxy.name?.data) { if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8'); proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else { } else {
proxy.name = utf8ArrayToStr(proxy.name); proxy.name = Buffer.from(proxy.name).toString('utf8');
} }
} catch (e) { } catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`); $.error(`proxy.name decode failed\nReason: ${e}`);
@@ -438,9 +510,46 @@ function lastParse(proxy) {
} }
} }
} }
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)) { if (['', 'off'].includes(proxy.sni)) {
proxy['disable-sni'] = true; 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);
}
return proxy; return proxy;
} }

View File

@@ -5,6 +5,7 @@ import {
isPresent, isPresent,
isNotBlank, isNotBlank,
getIfPresent, getIfPresent,
getRandomPort,
} from '@/utils'; } from '@/utils';
import getSurgeParser from './peggy/surge'; import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon'; import getLoonParser from './peggy/loon';
@@ -13,6 +14,19 @@ import getTrojanURIParser from './peggy/trojan-uri';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
function surge_port_hopping(raw) {
const [parts, port_hopping] =
raw.match(
/,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
) || [];
return {
port_hopping: port_hopping
? port_hopping.replace(/;/g, ',')
: undefined,
line: parts ? raw.replace(parts, '') : raw,
};
}
// Parse SS URI format (only supports new SIP002, legacy format is depreciated). // Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme // reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() { function URI_SS() {
@@ -158,7 +172,7 @@ function URI_SSR() {
for (const item of line) { for (const item of line) {
let [key, val] = item.split('='); let [key, val] = item.split('=');
val = val.trim(); val = val.trim();
if (val.length > 0) { if (val.length > 0 && val !== '(null)') {
other_params[key] = val; other_params[key] = val;
} }
} }
@@ -296,6 +310,11 @@ function URI_VMess() {
? !params.verify_cert ? !params.verify_cert
: undefined, : undefined,
}; };
if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(
params.allowInsecure,
);
}
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) { if (proxy.tls && proxy.sni) {
proxy.sni = params.sni; proxy.sni = params.sni;
@@ -305,8 +324,9 @@ function URI_VMess() {
if (params.net === 'ws' || params.obfs === 'websocket') { if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws'; proxy.network = 'ws';
} else if ( } else if (
['tcp', 'http'].includes(params.net) || ['http'].includes(params.net) ||
params.obfs === 'http' ['http'].includes(params.obfs) ||
['http'].includes(params.type)
) { ) {
proxy.network = 'http'; proxy.network = 'http';
} else if (['grpc'].includes(params.net)) { } else if (['grpc'].includes(params.net)) {
@@ -317,6 +337,8 @@ function URI_VMess() {
) { ) {
proxy.network = 'ws'; proxy.network = 'ws';
httpupgrade = true; httpupgrade = true;
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
} }
if (proxy.network) { if (proxy.network) {
let transportHost = params.host ?? params.obfsParam; let transportHost = params.host ?? params.obfsParam;
@@ -332,6 +354,10 @@ function URI_VMess() {
if (proxy.network === 'http') { if (proxy.network === 'http') {
if (transportHost) { if (transportHost) {
// 1)http(tcp)->host中间逗号(,)隔开
transportHost = transportHost
.split(',')
.map((i) => i.trim());
transportHost = Array.isArray(transportHost) transportHost = Array.isArray(transportHost)
? transportHost[0] ? transportHost[0]
: transportHost; : transportHost;
@@ -344,6 +370,7 @@ function URI_VMess() {
transportPath = '/'; transportPath = '/';
} }
} }
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
if (transportPath || transportHost) { if (transportPath || transportHost) {
if (['grpc'].includes(proxy.network)) { if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = { proxy[`${proxy.network}-opts`] = {
@@ -532,13 +559,42 @@ function URI_Hysteria2() {
}; };
const parse = (line) => { const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2]; line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars // 端口跳跃有两种写法:
let [__, password, server, ___, port, ____, addons = '', name] = // 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); // 端口部分支持 端口跳跃 的「多端口地址格式」。
port = parseInt(`${port}`, 10); // https://hysteria.network/zh/docs/advanced/Port-Hopping
if (isNaN(port)) { // 2. 参数 mport
let ports;
/* eslint-disable no-unused-vars */
let [
__,
password,
server,
___,
port,
____,
_____,
______,
_______,
________,
addons = '',
name,
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
line,
);
/* eslint-enable no-unused-vars */
if (/^\d+$/.test(port)) {
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
} else if (port) {
ports = port;
port = getRandomPort(ports);
} else {
port = 443; port = 443;
} }
password = decodeURIComponent(password); password = decodeURIComponent(password);
if (name != null) { if (name != null) {
name = decodeURIComponent(name); name = decodeURIComponent(name);
@@ -550,6 +606,7 @@ function URI_Hysteria2() {
name, name,
server, server,
port, port,
ports,
password, password,
}; };
@@ -1282,7 +1339,12 @@ function Surge_Tuic() {
const test = (line) => { const test = (line) => {
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]); return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
return { name, test, parse }; return { name, test, parse };
} }
function Surge_WireGuard() { function Surge_WireGuard() {
@@ -1299,7 +1361,12 @@ function Surge_Hysteria2() {
const test = (line) => { const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]); return /^.*=\s*hysteria2/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
return { name, test, parse }; return { name, test, parse };
} }

View File

@@ -54,31 +54,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* { vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
handleTransport(); handleTransport();
} }
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "vless"; proxy.type = "vless";
handleTransport(); handleTransport();
} }
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* { hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
} }
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
} }
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -172,12 +172,15 @@ vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseIn
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; } tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _

View File

@@ -52,31 +52,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* { vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
handleTransport(); handleTransport();
} }
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "vless"; proxy.type = "vless";
handleTransport(); handleTransport();
} }
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* { hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
} }
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
} }
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* { socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -170,12 +170,15 @@ vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseIn
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; } tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _

View File

@@ -91,11 +91,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
} }
handleShadowTLS(); 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/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
handleShadowTLS(); 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/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS(); handleShadowTLS();
@@ -104,7 +104,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS(); 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/others)* { hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
handleShadowTLS(); handleShadowTLS();
} }
@@ -151,6 +151,8 @@ port = digits:[0-9]+ {
} }
} }
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
username = & { username = & {
let j = peg$currPos; let j = peg$currPos;
let start, end; let start, end;

View File

@@ -89,11 +89,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
} }
handleShadowTLS(); 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/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
handleShadowTLS(); 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/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS(); handleShadowTLS();
@@ -102,7 +102,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS(); 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/others)* { hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
handleShadowTLS(); handleShadowTLS();
} }
@@ -149,6 +149,8 @@ port = digits:[0-9]+ {
} }
} }
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
username = & { username = & {
let j = peg$currPos; let j = peg$currPos;
let start, end; let start, end;

View File

@@ -1,13 +1,15 @@
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache'; import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils'; import { isIPv4, isIPv6, ipAddress } from '@/utils';
import { FULL } from '@/utils/logical'; import { FULL } from '@/utils/logical';
import { getFlag, removeFlag } from '@/utils/geo'; import { getFlag, removeFlag } from '@/utils/geo';
import { doh } from '@/utils/dns';
import lodash from 'lodash'; import lodash from 'lodash';
import $ from '@/core/app'; import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import env from '@/utils/env'; import env from '@/utils/env';
import { import {
@@ -314,7 +316,7 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`! 1. This function name should be `operator`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptOperator(script, targetPlatform, $arguments, source) { function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
return { return {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -324,6 +326,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
'operator', 'operator',
script, script,
$arguments, $arguments,
$options,
); );
output = operator(proxies, targetPlatform, { source, ...env }); output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -336,9 +339,9 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
'operator', 'operator',
`async function operator(input = []) { `async function operator(input = []) {
if (input && (input.$files || input.$content)) { if (input && (input.$files || input.$content)) {
let { $content, $files } = input let { $content, $files, $options } = input
${script} ${script}
return { $content, $files } return { $content, $files, $options }
} else { } else {
let proxies = input let proxies = input
let list = [] let list = []
@@ -350,6 +353,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
} }
}`, }`,
$arguments, $arguments,
$options,
); );
output = operator(proxies, targetPlatform, { source, ...env }); output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -362,9 +366,6 @@ function parseIP4P(IP4P) {
let server; let server;
let port; let port;
try { try {
if (!/^2001::[^:]+:[^:]+:[^:]+$/.test(IP4P)) {
throw new Error(`Invalid IP4P: ${IP4P}`);
}
let array = IP4P.split(':'); let array = IP4P.split(':');
port = parseInt(array[2], 16); port = parseInt(array[2], 16);
@@ -389,31 +390,61 @@ function parseIP4P(IP4P) {
} }
const DOMAIN_RESOLVERS = { const DOMAIN_RESOLVERS = {
Google: async function (domain, type, noCache) { Custom: async function (domain, type, noCache, timeout, edns, url) {
const id = hex_md5(`GOOGLE:${domain}:${type}`); const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
url: `https://8.8.4.4/resolve?name=${encodeURIComponent( const res = await doh({
domain, url,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`, domain,
headers: { type: answerType,
accept: 'application/dns-json', timeout,
}, edns,
}); });
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) { const { answers } = res;
throw new Error(`Status is ${body['Status']}`); if (!Array.isArray(answers) || answers.length === 0) {
} throw new Error('No answers');
const answers = body['Answer']; }
if (answers.length === 0) { const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1].data;
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
'IP-API': async function (domain, type, noCache) { Google: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
const res = await doh({
url: 'https://8.8.4.4/dns-query',
domain,
type: answerType,
timeout,
edns,
});
const { answers } = res;
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain, type, noCache, timeout) {
if (['IPv6'].includes(type)) { if (['IPv6'].includes(type)) {
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`); throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
} }
@@ -424,91 +455,124 @@ const DOMAIN_RESOLVERS = {
url: `http://ip-api.com/json/${encodeURIComponent( url: `http://ip-api.com/json/${encodeURIComponent(
domain, domain,
)}?lang=zh-CN`, )}?lang=zh-CN`,
timeout,
}); });
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
if (body['status'] !== 'success') { if (body['status'] !== 'success') {
throw new Error(`Status is ${body['status']}`); throw new Error(`Status is ${body['status']}`);
} }
const result = body.query; if (!body.query || body.query === 0) {
throw new Error('No answers');
}
const result = [body.query];
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Cloudflare: async function (domain, type, noCache) { Cloudflare: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`); const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent( const res = await doh({
domain, url: 'https://1.0.0.1/dns-query',
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`, domain,
headers: { type: answerType,
accept: 'application/dns-json', timeout,
}, edns,
}); });
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) { const { answers } = res;
throw new Error(`Status is ${body['Status']}`); if (!Array.isArray(answers) || answers.length === 0) {
} throw new Error('No answers');
const answers = body['Answer']; }
if (answers.length === 0) { const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1].data;
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Ali: async function (domain, type, noCache) { Ali: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`ALI:${domain}:${type}`); const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://223.6.6.6/resolve?edns_client_subnet=223.6.6.6/24&name=${encodeURIComponent( url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/24&name=${encodeURIComponent(
domain, domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`, )}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
timeout,
}); });
const answers = JSON.parse(resp.body); const answers = JSON.parse(resp.body);
if (answers.length === 0) { if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers;
if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1];
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Tencent: async function (domain, type, noCache) { Tencent: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`ALI:${domain}:${type}`); const id = hex_md5(`TENCENT:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const resp = await $.http.get({
url: `http://119.28.28.28/d?ip=119.28.28.28&type=${ url: `http://119.28.28.28/d?ip=${edns}&type=${
type === 'IPv6' ? 'AAAA' : 'A' type === 'IPv6' ? 'AAAA' : 'A'
}&dn=${encodeURIComponent(domain)}`, }&dn=${encodeURIComponent(domain)}`,
headers: { headers: {
accept: 'application/dns-json', accept: 'application/dns-json',
}, },
timeout,
}); });
const answers = resp.body.split(';').map((i) => i.split(',')[0]); const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0 || String(answers) === '0') { if (answers.length === 0 || String(answers) === '0') {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers[answers.length - 1]; const result = answers;
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
}; };
function ResolveDomainOperator({ provider, type: _type, filter, cache }) { function ResolveDomainOperator({
provider,
type: _type,
filter,
cache,
url,
timeout,
edns: _edns,
}) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) { if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`); throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
} }
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4'; let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider]; const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) { if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`); throw new Error(`找不到域名解析服务提供方: ${provider}`);
} }
let edns = _edns || '223.6.6.6';
if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
$.info(
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,
);
return { return {
name: 'Resolve Domain Operator', name: 'Resolve Domain Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -531,7 +595,14 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
const currentBatch = []; const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) { for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push( currentBatch.push(
resolver(domain, type, cache === 'disabled') resolver(
domain,
type,
cache === 'disabled',
requestTimeout,
edns,
url,
)
.then((ip) => { .then((ip) => {
results[domain] = ip; results[domain] = ip;
$.info( $.info(
@@ -550,32 +621,56 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
proxies.forEach((p) => { proxies.forEach((p) => {
if (!p['_no-resolve']) { if (!p['_no-resolve']) {
if (results[p.server]) { if (results[p.server]) {
if (_type === 'IP4P') { p._resolved_ips = results[p.server];
const { server, port } = parseIP4P( let ip = Array.isArray(results[p.server])
results[p.server], ? results[p.server][
); Math.floor(
if (server && port) { Math.random() * results[p.server].length,
)
]
: results[p.server];
if (type === 'IPv6' && isIPv6(ip)) {
try {
ip = new ipAddress.Address6(ip).correctForm();
} catch (e) {
$.error(
`Failed to parse IPv6 address: ${ip}: ${e}`,
);
}
if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) {
p._IP4P = ip;
const { server, port } = parseIP4P(ip);
if (server && port) {
p._domain = p.server;
p.server = server;
p.port = port;
p.resolved = true;
p._IPv4 = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
} else if (!p.resolved) {
p.resolved = false;
}
} else {
p._domain = p.server; p._domain = p.server;
p.server = server; p.server = ip;
p.port = port;
p.resolved = true; p.resolved = true;
p._IPv4 = p.server; p[`_${type}`] = p.server;
if (!isIP(p._IP)) { if (!isIP(p._IP)) {
p._IP = p.server; p._IP = p.server;
} }
} else {
p.resolved = false;
} }
} else { } else {
p._domain = p.server; p._domain = p.server;
p.server = results[p.server]; p.server = ip;
p.resolved = true; p.resolved = true;
p[`_${type}`] = p.server; p[`_${type}`] = p.server;
if (!isIP(p._IP)) { if (!isIP(p._IP)) {
p._IP = p.server; p._IP = p.server;
} }
} }
} else { } else if (!p.resolved) {
p.resolved = false; p.resolved = false;
} }
} }
@@ -697,7 +792,7 @@ function TypeFilter(types) {
1. This function name should be `filter`! 1. This function name should be `filter`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptFilter(script, targetPlatform, $arguments, source) { function ScriptFilter(script, targetPlatform, $arguments, source, $options) {
return { return {
name: 'Script Filter', name: 'Script Filter',
func: async (proxies) => { func: async (proxies) => {
@@ -707,6 +802,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
'filter', 'filter',
script, script,
$arguments, $arguments,
$options,
); );
output = filter(proxies, targetPlatform, { source, ...env }); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -729,6 +825,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
return list return list
}`, }`,
$arguments, $arguments,
$options,
); );
output = filter(proxies, targetPlatform, { source, ...env }); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -869,7 +966,7 @@ function clone(object) {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
function createDynamicFunction(name, script, $arguments) { function createDynamicFunction(name, script, $arguments, $options) {
const flowUtils = { const flowUtils = {
getFlowField, getFlowField,
getFlowHeaders, getFlowHeaders,
@@ -881,6 +978,7 @@ function createDynamicFunction(name, script, $arguments) {
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(
'$arguments', '$arguments',
'$options',
'$substore', '$substore',
'lodash', 'lodash',
'$persistentStore', '$persistentStore',
@@ -890,9 +988,11 @@ function createDynamicFunction(name, script, $arguments) {
'scriptResourceCache', 'scriptResourceCache',
'flowUtils', 'flowUtils',
'produceArtifact', 'produceArtifact',
'require',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
$options,
$, $,
lodash, lodash,
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@@ -905,26 +1005,30 @@ function createDynamicFunction(name, script, $arguments) {
scriptResourceCache, scriptResourceCache,
flowUtils, flowUtils,
produceArtifact, produceArtifact,
eval(`typeof require !== "undefined"`) ? require : undefined,
); );
} else { } else {
return new Function( return new Function(
'$arguments', '$arguments',
'$options',
'$substore', '$substore',
'lodash', 'lodash',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils', 'flowUtils',
'produceArtifact', 'produceArtifact',
'require',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
$options,
$, $,
lodash, lodash,
ProxyUtils, ProxyUtils,
scriptResourceCache, scriptResourceCache,
flowUtils, flowUtils,
produceArtifact, produceArtifact,
eval(`typeof require !== "undefined"`) ? require : undefined,
); );
} }
} }

View File

@@ -133,9 +133,13 @@ export default function Clash_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }
@@ -159,9 +163,11 @@ export default function Clash_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -149,9 +149,13 @@ export default function ClashMeta_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }
@@ -174,9 +178,11 @@ export default function ClashMeta_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -153,6 +153,14 @@ function trojan(proxy) {
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -215,6 +223,14 @@ function vmess(proxy) {
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// AEAD // AEAD
if (isPresent(proxy, 'aead')) { if (isPresent(proxy, 'aead')) {
@@ -286,6 +302,14 @@ function vless(proxy) {
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -339,6 +363,11 @@ function socks5(proxy) {
// tfo // tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString(); return result.toString();
} }
@@ -408,8 +437,8 @@ function wireguard(proxy) {
} }
function hysteria2(proxy) { function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) { if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
throw new Error(`obfs is unsupported`); throw new Error(`only salamander obfs is supported`);
} }
const result = new Result(proxy); const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`); result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
@@ -418,11 +447,23 @@ function hysteria2(proxy) {
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
result.appendIfPresent( result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`, `,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify', 'skip-cert-verify',
); );
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
result.append(`,salamander-password=${proxy['obfs-password']}`);
}
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');

View File

@@ -152,9 +152,13 @@ export default function ShadowRocket_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }
@@ -177,9 +181,11 @@ export default function ShadowRocket_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -202,13 +202,8 @@ const tlsParser = (proxy, parsedProxy) => {
parsedProxy.tls.alpn = [proxy.alpn]; parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) 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) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr; if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str']; if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (proxy['reality-opts']) { if (proxy['reality-opts']) {
parsedProxy.tls.reality = { enabled: true }; parsedProxy.tls.reality = { enabled: true };
if (proxy['reality-opts']['public-key']) if (proxy['reality-opts']['public-key'])
@@ -217,7 +212,13 @@ const tlsParser = (proxy, parsedProxy) => {
if (proxy['reality-opts']['short-id']) if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id = parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id']; proxy['reality-opts']['short-id'];
parsedProxy.tls.utls = { enabled: true };
} }
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (!parsedProxy.tls.enabled) delete parsedProxy.tls; if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
}; };

View File

@@ -21,6 +21,8 @@ export default function Stash_Producer() {
'wireguard', 'wireguard',
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'ssh',
'juicity',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'ss' && (proxy.type === 'ss' &&
![ ![
@@ -232,9 +234,13 @@ export default function Stash_Producer() {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
} }
@@ -266,9 +272,11 @@ export default function Stash_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -15,6 +15,9 @@ const ipVersions = {
export default function Surge_Producer() { export default function Surge_Producer() {
const produce = (proxy, type, opts = {}) => { const produce = (proxy, type, opts = {}) => {
proxy.name = proxy.name.replace(/=|,/g, ''); proxy.name = proxy.name.replace(/=|,/g, '');
if (proxy.ports) {
proxy.ports = String(proxy.ports);
}
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
return shadowsocks(proxy); return shadowsocks(proxy);
@@ -675,6 +678,15 @@ function tuic(proxy) {
'alpn', 'alpn',
); );
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -935,6 +947,15 @@ function hysteria2(proxy) {
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');

View File

@@ -1,6 +1,8 @@
import { Result } from './utils'; import { Base64 } from 'js-base64';
import { Result, isPresent } from './utils';
import Surge_Producer from './surge'; import Surge_Producer from './surge';
import { isIPv4, isIPv6, isPresent } from '@/utils'; import ClashMeta_Producer from './clashmeta';
import { isIPv4, isIPv6 } from '@/utils';
import $ from '@/core/app'; import $ from '@/core/app';
const targetPlatform = 'SurgeMac'; const targetPlatform = 'SurgeMac';
@@ -8,14 +10,22 @@ const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer(); const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() { export default function SurgeMac_Producer() {
const produce = (proxy) => { const produce = (proxy, type, opts = {}) => {
switch (proxy.type) { switch (proxy.type) {
case 'external': case 'external':
return external(proxy); return external(proxy);
case 'ssr': // case 'ssr':
return shadowsocksr(proxy); // return shadowsocksr(proxy);
default: default: {
return surge_Producer.produce(proxy); try {
return surge_Producer.produce(proxy, type, opts);
} catch (e) {
$.log(
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
);
return mihomo(proxy, type, opts);
}
}
} }
}; };
return { produce }; return { produce };
@@ -60,6 +70,7 @@ function external(proxy) {
return result.toString(); return result.toString();
} }
// eslint-disable-next-line no-unused-vars
function shadowsocksr(proxy) { function shadowsocksr(proxy) {
const external_proxy = { const external_proxy = {
...proxy, ...proxy,
@@ -84,6 +95,7 @@ function shadowsocksr(proxy) {
for (const [key, value] of Object.entries({ for (const [key, value] of Object.entries({
cipher: '-m', cipher: '-m',
obfs: '-o', obfs: '-o',
'obfs-param': '-g',
password: '-k', password: '-k',
port: '-p', port: '-p',
protocol: '-O', protocol: '-O',
@@ -92,12 +104,73 @@ function shadowsocksr(proxy) {
'local-port': '-l', 'local-port': '-l',
'local-address': '-b', 'local-address': '-b',
})) { })) {
external_proxy.args.push(value); if (external_proxy[key] != null) {
external_proxy.args.push(external_proxy[key]); external_proxy.args.push(value);
external_proxy.args.push(external_proxy[key]);
}
} }
return external(external_proxy); return external(external_proxy);
} }
// eslint-disable-next-line no-unused-vars
function mihomo(proxy, type, opts) {
const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];
if (clashProxy) {
const localPort = opts?.localPort || proxy._localPort || 65535;
const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])
? false
: true;
const external_proxy = {
name: proxy.name,
type: 'external',
exec: proxy._exec || '/usr/local/bin/mihomo',
'local-port': localPort,
args: [
'-config',
Base64.encode(
JSON.stringify({
'mixed-port': localPort,
ipv6,
mode: 'global',
dns: {
enable: true,
ipv6,
nameserver: [
'https://223.6.6.6/dns-query',
'https://120.53.53.53/dns-query',
],
},
proxies: [
{
...clashProxy,
name: 'proxy',
},
],
'proxy-groups': [
{
name: 'GLOBAL',
type: 'select',
proxies: ['proxy'],
},
],
}),
),
],
addresses: [],
};
// https://manual.nssurge.com/policy/external-proxy.html
if (isIP(proxy.server)) {
external_proxy.addresses.push(proxy.server);
} else {
$.log(
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
);
}
opts.localPort = localPort - 1;
return external(external_proxy);
}
}
function isIP(ip) { function isIP(ip) {
return isIPv4(ip) || isIPv6(ip); return isIPv4(ip) || isIPv6(ip);

View File

@@ -16,7 +16,11 @@ export default function URI_Producer() {
delete proxy[key]; delete proxy[key];
} }
} }
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) { if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy.server && isIPv6(proxy.server)) { if (proxy.server && isIPv6(proxy.server)) {

View File

@@ -1,12 +1,30 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import URI_Producer from './uri'; import URI_Producer from './uri';
import $ from '@/core/app';
const URI = URI_Producer(); const URI = URI_Producer();
export default function V2Ray_Producer() { export default function V2Ray_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => const produce = (proxies) => {
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n')); let result = [];
proxies.map((proxy) => {
try {
result.push(URI.produce(proxy));
} catch (err) {
$.error(
`Cannot produce proxy: ${JSON.stringify(
proxy,
null,
2,
)}\nReason: ${err}`,
);
}
});
return Base64.encode(result.join('\n'));
};
return { type, produce }; return { type, produce };
} }

View File

@@ -70,6 +70,24 @@ async function downloadSubscription(req, res) {
includeUnsupportedProxy, includeUnsupportedProxy,
resultFormat, resultFormat,
} = req.query; } = req.query;
let $options = {};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`); $.info(`指定远程订阅 URL: ${url}`);
@@ -116,6 +134,7 @@ async function downloadSubscription(req, res) {
produceOpts: { produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy, 'include-unsupported-proxy': includeUnsupportedProxy,
}, },
$options,
}); });
if ( if (
@@ -123,10 +142,11 @@ async function downloadSubscription(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) { ) {
try { try {
url = `${url || sub.url}` url =
.split(/[\r\n]+/) `${url || sub.url}`
.map((i) => i.trim()) .split(/[\r\n]+/)
.filter((i) => i.length)?.[0]; .map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -246,6 +266,25 @@ async function downloadCollection(req, res) {
resultFormat, resultFormat,
} = req.query; } = req.query;
let $options = {};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub); ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`); $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
@@ -271,6 +310,7 @@ async function downloadCollection(req, res) {
produceOpts: { produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy, 'include-unsupported-proxy': includeUnsupportedProxy,
}, },
$options,
}); });
// forward flow header from the first subscription in this collection // forward flow header from the first subscription in this collection
@@ -283,10 +323,11 @@ async function downloadCollection(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources) ['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) { ) {
try { try {
let url = `${sub.url}` let url =
.split(/[\r\n]+/) `${sub.url}`
.map((i) => i.trim()) .split(/[\r\n]+/)
.filter((i) => i.length)?.[0]; .map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');

View File

@@ -1,4 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { FILES_KEY } from '@/constants'; import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -50,7 +51,33 @@ async function getFile(req, res) {
name = decodeURIComponent(name); name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`); $.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query; let {
url,
subInfoUrl,
subInfoUserAgent,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
} = req.query;
let $options = {};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`); $.info(`指定远程文件 URL: ${url}`);
@@ -59,6 +86,14 @@ async function getFile(req, res) {
ua = decodeURIComponent(ua); ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`); $.info(`指定远程文件 User-Agent: ${ua}`);
} }
if (subInfoUrl) {
subInfoUrl = decodeURIComponent(subInfoUrl);
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
}
if (subInfoUserAgent) {
subInfoUserAgent = decodeURIComponent(subInfoUserAgent);
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
}
if (content) { if (content) {
content = decodeURIComponent(content); content = decodeURIComponent(content);
$.info(`指定本地文件: ${content}`); $.info(`指定本地文件: ${content}`);
@@ -84,8 +119,36 @@ async function getFile(req, res) {
content, content,
mergeSources, mergeSources,
ignoreFailedRemoteFile, ignoreFailedRemoteFile,
$options,
}); });
try {
subInfoUrl = subInfoUrl || file.subInfoUrl;
if (subInfoUrl) {
// forward flow headers
const flowInfo = await getFlowHeaders(
subInfoUrl,
subInfoUserAgent || file.subInfoUserAgent,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
`文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
if (file.download) {
res.set(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(
file.displayName || file.name,
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8').send( res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '', output ?? '',
); );

View File

@@ -3,6 +3,7 @@ import $ from '@/core/app';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import download from '@/utils/download'; import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync'; import { syncArtifacts } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
@@ -44,20 +45,81 @@ export default function serve() {
$app.start(); $app.start();
if ($.env.isNode) { if ($.env.isNode) {
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON'); // Deprecated: SUB_STORE_BACKEND_CRON
if (backend_cron) { const backend_sync_cron =
$.info(`[CRON] ${backend_cron} enabled`); eval('process.env.SUB_STORE_BACKEND_SYNC_CRON') ||
eval('process.env.SUB_STORE_BACKEND_CRON');
if (backend_sync_cron) {
$.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
const { CronJob } = eval(`require("cron")`); const { CronJob } = eval(`require("cron")`);
new CronJob( new CronJob(
backend_cron, backend_sync_cron,
async function () { async function () {
try { try {
$.info(`[CRON] ${backend_cron} started`); $.info(`[SYNC CRON] ${backend_sync_cron} started`);
await syncArtifacts(); await syncArtifacts();
$.info(`[CRON] ${backend_cron} finished`); $.info(`[SYNC CRON] ${backend_sync_cron} finished`);
} catch (e) { } catch (e) {
$.error( $.error(
`[CRON] ${backend_cron} error: ${e.message ?? e}`, `[SYNC CRON] ${backend_sync_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 }, // onTick

View File

@@ -80,10 +80,72 @@ async function refresh(_, res) {
success(res); success(res);
} }
async function gistBackupAction(action) {
// read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) throw new Error('GitHub Token is required for backup!');
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
$.info(`上传备份完成`);
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (Object.keys(JSON.parse(content).settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
}
async function gistBackup(req, res) { async function gistBackup(req, res) {
const { action } = req.query; const { action } = req.query;
// read token // read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
failed( failed(
res, res,
@@ -93,68 +155,8 @@ async function gistBackup(req, res) {
), ),
); );
} else { } else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
try { try {
let content; await gistBackupAction(action);
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (
Object.keys(JSON.parse(content).settings).length ===
0
) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
success(res); success(res);
} catch (err) { } catch (err) {
$.error( $.error(
@@ -171,3 +173,5 @@ async function gistBackup(req, res) {
} }
} }
} }
export { gistBackupAction };

View File

@@ -146,7 +146,8 @@ async function compareSub(req, res) {
// add id // add id
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
proxy.id = i; proxy.id = i;
proxy.subName = sub.name; proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
}); });
// apply processors // apply processors
@@ -237,8 +238,10 @@ async function compareCollection(req, res) {
.flat(); .flat();
currentProxies.forEach((proxy) => { currentProxies.forEach((proxy) => {
proxy.subName = sub.name; proxy._subName = sub.name;
proxy.collectionName = collection.name; proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
// apply processors // apply processors
@@ -276,7 +279,8 @@ async function compareCollection(req, res) {
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
proxy.id = i; proxy.id = i;
proxy.collectionName = collection.name; proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
const processed = await ProxyUtils.process( const processed = await ProxyUtils.process(

View File

@@ -34,6 +34,11 @@ export default function register($app) {
async function getFlowInfo(req, res) { async function getFlowInfo(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
let { url } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
if (!sub) { if (!sub) {
@@ -68,10 +73,11 @@ async function getFlowInfo(req, res) {
return; return;
} }
try { try {
let url = `${sub.url}` url =
.split(/[\r\n]+/) `${url || sub.url}`
.map((i) => i.trim()) .split(/[\r\n]+/)
.filter((i) => i.length)?.[0]; .map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');

View File

@@ -37,6 +37,7 @@ async function produceArtifact({
produceOpts = {}, produceOpts = {},
subscription, subscription,
awaitCustomCache, awaitCustomCache,
$options,
}) { }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
@@ -150,7 +151,8 @@ async function produceArtifact({
.flat(); .flat();
proxies.forEach((proxy) => { proxies.forEach((proxy) => {
proxy.subName = sub.name; proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
}); });
// apply processors // apply processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
@@ -158,6 +160,7 @@ async function produceArtifact({
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub }, { [sub.name]: sub },
$options,
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`); throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -250,8 +253,10 @@ async function produceArtifact({
.flat(); .flat();
currentProxies.forEach((proxy) => { currentProxies.forEach((proxy) => {
proxy.subName = sub.name; proxy._subName = sub.name;
proxy.collectionName = collection.name; proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
// apply processors // apply processors
@@ -259,7 +264,11 @@ async function produceArtifact({
currentProxies, currentProxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub, _collection: collection }, {
[sub.name]: sub,
_collection: collection,
$options,
},
); );
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
@@ -303,7 +312,8 @@ async function produceArtifact({
); );
proxies.forEach((proxy) => { proxies.forEach((proxy) => {
proxy.collectionName = collection.name; proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
// apply own processors // apply own processors
@@ -312,6 +322,7 @@ async function produceArtifact({
collection.process || [], collection.process || [],
platform, platform,
{ _collection: collection }, { _collection: collection },
$options,
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`); throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -333,6 +344,7 @@ async function produceArtifact({
} }
exist[proxy.name] = true; exist[proxy.name] = true;
} }
console.log(proxies);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts); return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'rule') { } else if (type === 'rule') {
const allRules = $.read(RULES_KEY); const allRules = $.read(RULES_KEY);
@@ -460,10 +472,10 @@ async function produceArtifact({
const processed = const processed =
Array.isArray(file.process) && file.process.length > 0 Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process( ? await ProxyUtils.process(
{ $files: files, $content: filesContent }, { $files: files, $content: filesContent, $options },
file.process, file.process,
) )
: { $content: filesContent, $files: files }; : { $content: filesContent, $files: files, $options };
return processed?.$content ?? ''; return processed?.$content ?? '';
} }

49
backend/src/utils/dns.js Normal file
View File

@@ -0,0 +1,49 @@
import $ from '@/core/app';
import dnsPacket from 'dns-packet';
import { Buffer } from 'buffer';
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: 24,
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));
}

View File

@@ -15,10 +15,10 @@ import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download( export default async function download(
rawUrl, rawUrl = '',
ua, ua,
timeout, timeout,
proxy, customProxy,
skipCustomCache, skipCustomCache,
awaitCustomCache, awaitCustomCache,
) { ) {
@@ -43,12 +43,21 @@ export default async function download(
} }
} }
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV(); const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } = const { defaultProxy, defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY); $.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta'; const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout; const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url); const id = hex_md5(userAgent + url);
if ($arguments?.cacheKey === true) {
$.error(`使用自定义缓存时 cacheKey 的值不能为空`);
$arguments.cacheKey = undefined;
}
const customCacheKey = $arguments?.cacheKey const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}` ? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined; : undefined;

View File

@@ -10,8 +10,14 @@ export function getFlowField(headers) {
)[0]; )[0];
return headers[subkey]; return headers[subkey];
} }
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) { export async function getFlowHeaders(
let url = flowUrl || rawUrl; rawUrl,
ua,
timeout,
customProxy,
flowUrl,
) {
let url = flowUrl || rawUrl || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
url = url.split('#')[0]; url = url.split('#')[0];
@@ -41,7 +47,13 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
// $.info(`使用缓存的流量信息: ${url}`); // $.info(`使用缓存的流量信息: ${url}`);
flowInfo = cached; flowInfo = cached;
} else { } else {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY); 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 = const userAgent =
ua || ua ||
defaultFlowUserAgent || defaultFlowUserAgent ||
@@ -67,7 +79,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
$.info( $.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${ `使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || '' userAgent || ''
}`, }, Proxy: ${proxy}`,
); );
const { headers } = await http.head({ const { headers } = await http.head({
url: url url: url
@@ -97,14 +109,14 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
$.error( $.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${ `使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || '' userAgent || ''
}: ${e.message ?? e}`, }, Proxy: ${proxy}: ${e.message ?? e}`,
); );
} }
if (!flowInfo) { if (!flowInfo) {
$.info( $.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${ `使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || '' userAgent || ''
}`, }, Proxy: ${proxy}`,
); );
const { headers } = await http.get({ const { headers } = await http.get({
url: url url: url
@@ -113,8 +125,21 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
.filter((i) => i.length)[0], .filter((i) => i.length)[0],
headers: { headers: {
'User-Agent': userAgent, 'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
}, },
timeout: requestTimeout, timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
}); });
flowInfo = getFlowField(headers); flowInfo = getFlowField(headers);
} }

View File

@@ -293,7 +293,7 @@ export function getFlag(name) {
'沪俄', '沪俄',
'Moscow', 'Moscow',
], ],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'], '🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
'🇸🇪': ['Sweden', '瑞典'], '🇸🇪': ['Sweden', '瑞典'],
'🇸🇬': [ '🇸🇬': [
'Singapore', 'Singapore',
@@ -329,6 +329,7 @@ export function getFlag(name) {
'台', '台',
'臺', '臺',
'Taipei', 'Taipei',
'Tai Wan',
], ],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'], '🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
'🇺🇸': [ '🇺🇸': [

View File

@@ -1,10 +1,21 @@
import { HTTP } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import { SETTINGS_KEY } from '@/constants';
/** /**
* Gist backup * Gist backup
*/ */
export default class Gist { export default class Gist {
constructor({ token, key, syncPlatform }) { constructor({ token, key, syncPlatform }) {
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultProxy, defaultTimeout: timeout } = $.read(SETTINGS_KEY);
let proxy = defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
if (syncPlatform === 'gitlab') { if (syncPlatform === 'gitlab') {
this.headers = { this.headers = {
'PRIVATE-TOKEN': `${token}`, 'PRIVATE-TOKEN': `${token}`,
@@ -13,7 +24,25 @@ export default class Gist {
}; };
this.http = HTTP({ this.http = HTTP({
baseURL: 'https://gitlab.com/api/v4', baseURL: 'https://gitlab.com/api/v4',
headers: { ...this.headers }, 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,
events: { events: {
onResponse: (resp) => { onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) { if (/^[45]/.test(String(resp.statusCode))) {
@@ -35,7 +64,25 @@ export default class Gist {
}; };
this.http = HTTP({ this.http = HTTP({
baseURL: 'https://api.github.com', baseURL: 'https://api.github.com',
headers: { ...this.headers }, 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,
events: { events: {
onResponse: (resp) => { onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) { if (/^[45]/.test(String(resp.statusCode))) {

View File

@@ -1,3 +1,4 @@
import * as ipAddress from 'ip-address';
// source: https://stackoverflow.com/a/36760050 // source: https://stackoverflow.com/a/36760050
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/; const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
@@ -46,54 +47,78 @@ function getPolicyDescriptor(str) {
}; };
} }
const utf8ArrayToStr = // const utf8ArrayToStr =
typeof TextDecoder !== 'undefined' // typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v)) // ? (v) => new TextDecoder().decode(new Uint8Array(v))
: (function () { // : (function () {
var charCache = new Array(128); // Preallocate the cache for the common single byte chars // var charCache = new Array(128); // Preallocate the cache for the common single byte chars
var charFromCodePt = String.fromCodePoint || String.fromCharCode; // var charFromCodePt = String.fromCodePoint || String.fromCharCode;
var result = []; // var result = [];
return function (array) { // return function (array) {
var codePt, byte1; // var codePt, byte1;
var buffLen = array.length; // var buffLen = array.length;
result.length = 0; // result.length = 0;
for (var i = 0; i < buffLen; ) { // for (var i = 0; i < buffLen; ) {
byte1 = array[i++]; // byte1 = array[i++];
if (byte1 <= 0x7f) { // if (byte1 <= 0x7f) {
codePt = byte1; // codePt = byte1;
} else if (byte1 <= 0xdf) { // } else if (byte1 <= 0xdf) {
codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f); // codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
} else if (byte1 <= 0xef) { // } else if (byte1 <= 0xef) {
codePt = // codePt =
((byte1 & 0x0f) << 12) | // ((byte1 & 0x0f) << 12) |
((array[i++] & 0x3f) << 6) | // ((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f); // (array[i++] & 0x3f);
} else if (String.fromCodePoint) { // } else if (String.fromCodePoint) {
codePt = // codePt =
((byte1 & 0x07) << 18) | // ((byte1 & 0x07) << 18) |
((array[i++] & 0x3f) << 12) | // ((array[i++] & 0x3f) << 12) |
((array[i++] & 0x3f) << 6) | // ((array[i++] & 0x3f) << 6) |
(array[i++] & 0x3f); // (array[i++] & 0x3f);
} else { // } else {
codePt = 63; // Cannot convert four byte code points, so use "?" instead // codePt = 63; // Cannot convert four byte code points, so use "?" instead
i += 3; // i += 3;
} // }
result.push( // result.push(
charCache[codePt] || // charCache[codePt] ||
(charCache[codePt] = charFromCodePt(codePt)), // (charCache[codePt] = charFromCodePt(codePt)),
); // );
} // }
return result.join(''); // 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();
}
export { export {
ipAddress,
isIPv4, isIPv4,
isIPv6, isIPv6,
isValidPortNumber, isValidPortNumber,
@@ -101,6 +126,8 @@ export {
getIfNotBlank, getIfNotBlank,
isPresent, isPresent,
getIfPresent, getIfPresent,
utf8ArrayToStr, // utf8ArrayToStr,
getPolicyDescriptor, getPolicyDescriptor,
getRandomPort,
numberToString,
}; };

11
backend/src/utils/rs.js Normal file
View 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,
};

View File

@@ -28,7 +28,9 @@ export function getPlatformFromUserAgent({ ua, UA }) {
return 'Stash'; return 'Stash';
} else if ( } else if (
ua === 'meta' || ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) (ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) ||
ua.indexOf('clash-verge') !== -1 ||
ua.indexOf('flclash') !== -1
) { ) {
return 'ClashMeta'; return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) { } else if (ua.indexOf('clash') !== -1) {

View File

@@ -341,7 +341,10 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
const request = isNode const request = isNode
? eval("require('request')") ? eval("require('request')")
: $httpClient; : $httpClient;
const body = options.body;
const opts = JSON.parse(JSON.stringify(options)); const opts = JSON.parse(JSON.stringify(options));
opts.body = body;
if (!isNode && opts.timeout) { if (!isNode && opts.timeout) {
opts.timeout++; opts.timeout++;
let unit = 'ms'; let unit = 'ms';

37
config/Egern.yaml Normal file
View File

@@ -0,0 +1,37 @@
name: Sub-Store
description: "支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *"
compat_arguments:
ability: http-client-policy
cronexp: 55 23 * * *
sync: '"Sub-Store Sync"'
timeout: "120"
engine: auto
produce: '"# Sub-Store Produce"'
produce_cronexp: 50 */6 * * *
produce_sub: '"sub1,sub2"'
produce_col: '"col1,col2"'
compat_arguments_desc: '\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
scriptings:
- http_request:
name: Sub-Store Core
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
body_required: true
- http_request:
name: Sub-Store Simple
match: ^https?:\/\/sub\.store
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
body_required: true
- schedule:
name: "{{{sync}}}"
cron: "{{{cronexp}}}"
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
- schedule:
name: "{{{produce}}}"
cron: "{{{produce_cronexp}}}"
script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
arguments:
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
mitm:
hostnames:
- sub.store

View File

@@ -14,7 +14,7 @@ DOMAIN,sub-store.vercel.app,PROXY
hostname=sub.store hostname=sub.store
[Script] [Script]
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple http-request ^https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync cron "55 23 * * *" script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync

View File

@@ -2,6 +2,6 @@
"name": "Sub-Store", "name": "Sub-Store",
"description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'", "description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'",
"task": [ "task": [
"55 23 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" "55 23 * * * https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
] ]
} }

View File

@@ -1,4 +1,4 @@
hostname=sub.store hostname=sub.store
^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js ^https?:\/\/sub\.store url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js

View File

@@ -45,6 +45,9 @@ Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess
### 5. Shadowrocket ### 5. Shadowrocket
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。 安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。
### 6. Egern
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。
## 使用 Sub-Store ## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。 1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。 2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
@@ -56,4 +59,4 @@ https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%9
## 脚本使用说明 ## 脚本使用说明
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E

View File

@@ -25,13 +25,13 @@ cron:
script-providers: script-providers:
sub-store-0: sub-store-0:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
interval: 86400 interval: 86400
sub-store-1: sub-store-1:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
interval: 86400 interval: 86400
cron-sync-artifacts: cron-sync-artifacts:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
interval: 86400 interval: 86400

View File

@@ -8,10 +8,10 @@
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}} Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}} Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}} {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}" {{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

@@ -7,7 +7,7 @@ hostname = %APPEND% sub.store
[Script] [Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本 # 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120 Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120 Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js

View File

@@ -6,7 +6,7 @@
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120 Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js

View File

@@ -8,10 +8,10 @@
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}} Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}} Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}} {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}" {{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

@@ -1,28 +1,49 @@
function operator(proxies = [], targetPlatform, context) { function operator(proxies = [], targetPlatform, context) {
// 支持快捷操作 不一定要写一个 function // 支持快捷操作 不一定要写一个 function
// 可参考 https://t.me/zhetengsha/970 // 可参考 https://t.me/zhetengsha/970
// https://t.me/zhetengsha/1009 // https://t.me/zhetengsha/1009
// proxies 为传入的内部节点数组 // proxies 为传入的内部节点数组
// 结构大致参考了 Clash.Meta(mihomo) 有私货
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅 // 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货
// 1. `_no-resolve` 为不解析域名 // 1. `_no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `_resolved` 字段 // 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段 // 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置) // 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// 5. `_subName` 为单条订阅名 // 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名
// 6. `_collectionName` 为组合订阅名 // 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
// 7. `tls-fingerprint` 为 tls 指纹
// 8. `underlying-proxy` 为前置代理
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
// 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
// $arguments 为传入的脚本参数 // $arguments 为传入的脚本参数
// $options 为通过链接传入的参数
// 例如: { arg1: 'a', arg2: 'b' }
// 可这样传:
// 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))
// /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D
// 或这样传:
// 先这样处理 encodeURIComponent('arg1=a&arg2=b')
// /api/file/foo?$options=arg1%3Da%26arg2%3Db
// console.log($options)
// targetPlatform 为输出的目标平台 // targetPlatform 为输出的目标平台
// lodash // lodash
// $substore 为 OpenAPI // $substore 为 OpenAPI
// 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md // 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md
// scriptResourceCache 缓存 // scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003 // 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache // const cache = scriptResourceCache
@@ -35,6 +56,8 @@ function operator(proxies = [], targetPlatform, context) {
// parse, // 订阅解析 // parse, // 订阅解析
// process, // 节点操作/文件操作 // process, // 节点操作/文件操作
// produce, // 输出订阅 // produce, // 输出订阅
// getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000)
// ipAddress, // https://github.com/beaugunderson/ip-address
// isIPv4, // isIPv4,
// isIPv6, // isIPv6,
// isIP, // isIP,
@@ -43,8 +66,11 @@ function operator(proxies = [], targetPlatform, context) {
// removeFlag, // 移除 emoji 旗帜 // removeFlag, // 移除 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码 // getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类 // Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// } // }
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
// ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一
// 示例: 给节点名添加前缀 // 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}` // $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 给节点名添加旗帜 // 示例: 给节点名添加旗帜
@@ -68,7 +94,7 @@ function operator(proxies = [], targetPlatform, context) {
// } // }
// }) // })
// $content = proxies // $content = proxies
// 2. sing-box // 2. sing-box
// 但是一般不需要这样用, 可参考 // 但是一般不需要这样用, 可参考
@@ -102,8 +128,8 @@ function operator(proxies = [], targetPlatform, context) {
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist // 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
// 见 https://t.me/zhetengsha/1428 // 见 https://t.me/zhetengsha/1428
// //
// const content = ProxyUtils.produce(proxies, platform) // const content = ProxyUtils.produce([...proxies], platform)
// // YAML // // YAML
// ProxyUtils.yaml.load('YAML String') // ProxyUtils.yaml.load('YAML String')
@@ -122,16 +148,15 @@ function operator(proxies = [], targetPlatform, context) {
// yaml.proxies.unshift(...clashMetaProxies) // yaml.proxies.unshift(...clashMetaProxies)
// $content = ProxyUtils.yaml.dump(yaml) // $content = ProxyUtils.yaml.dump(yaml)
// { $content, $files, $options } will be passed to the next operator
// { $content, $files } will be passed to the next operator
// $content is the final content of the file // $content is the final content of the file
// flowUtils 为机场订阅流量信息处理工具 // flowUtils 为机场订阅流量信息处理工具
// 可参考: // 可参考:
// 1. https://t.me/zhetengsha/948 // 1. https://t.me/zhetengsha/948
// context 为传入的上下文 // context 为传入的上下文
// 有三种情况, 按需判断 // 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可)
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上 // 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
@@ -216,7 +241,7 @@ function operator(proxies = [], targetPlatform, context) {
// 参数说明 // 参数说明
// 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E // 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
console.log(JSON.stringify(context, null, 2)) console.log(JSON.stringify(context, null, 2));
return proxies return proxies;
} }