Compare commits

..

190 Commits

Author SHA1 Message Date
xream
c51f3511dd fix: 兼容部分不带节点名的 URI 2024-01-08 09:44:53 +08:00
xream
ee2fcc7ee3 fix: 兼容部分不带参数的 URI 输入 2024-01-08 09:28:33 +08:00
xream
95615d1877 feat: 支持全局请求超时(前端 > 2.14.29) 2024-01-08 07:22:03 +08:00
xream
962bcda9dd chore: 同步远程配置输出更多日志 2024-01-07 17:44:03 +08:00
xream
9bb4739d56 Node.js 版的通知支持第三方推送服务. 环境变量名 SUB_STORE_PUSH_SERVICE. 支持 Bark/PushPlus 等服务. 形如: https://api.day.app/XXXXXXXXX/[推送标题]/[推送内容]?group=SubStore&autoCopy=1&isArchive=1&sound=shake&level=timeSensitivehttp://www.pushplus.plus/send?token=XXXXXXXXX&title=[推送标题]&content=[推送内容]&channel=wechat 的 URL, [推送标题][推送内容] 会被自动替换 2024-01-02 22:52:33 +08:00
xream
de1d40f41a feat: Wireguard 结构跟进 Clash.Meta, allowed_ips 改为 allowed-ips 2024-01-02 16:38:48 +08:00
xream
c0ab301160 feat: Trojan URI 支持 gRPC 2023-12-29 16:08:02 +08:00
xream
a22df97a51 release: backend version 2.14.135 2023-12-29 15:42:31 +08:00
xream
45772ade4d Merge pull request #263 from Ariesly/ipv6-uri
fix: Handles node-info IPv6 address URIs
2023-12-29 15:39:03 +08:00
Ariesly
e8dab545f5 fix: Handles node-info IPv6 address URIs 2023-12-29 07:10:47 +00:00
xream
c2bd80207a doc: 补充文档 2023-12-27 02:55:04 +08:00
xream
bc5ae9a2ef feat: 支持 Surfboard(前端 > 2.14.27) 2023-12-27 00:28:15 +08:00
xream
36db057e32 feat: 当节点端口号为合法端口号时, 将类型转为整数(便于脚本判断) 2023-12-23 21:02:39 +08:00
xream
5ac73b863a feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:42:33 +08:00
xream
23042c33d6 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:41:37 +08:00
xream
4ca5f5e355 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:24:48 +08:00
xream
f10e5913fb feat: 兼容部分不规范的机场 Hysteria/Hysteria2 端口跳跃字段为空时 删除此字段 2023-12-17 18:31:12 +08:00
xream
8b75c11587 feat: Hysteria2 URI 输入支持 hy2:// 2023-12-17 16:13:34 +08:00
xream
c287dcad3b fix: 过滤 Stash/Clash Shadowsocks cipher 2023-12-13 20:11:36 +08:00
xream
ce6cd794c8 feat: 环境变量 SUB_STORE_DATA_URL 启动时自动从此地址拉取并恢复数据 2023-12-13 09:54:57 +08:00
xream
e05475aa5e feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 02:04:24 +08:00
xream
c35e9d37ae feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 01:26:16 +08:00
xream
8f2dbfe3df feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 00:34:08 +08:00
xream
a0a998dfdd feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:11 +08:00
xream
12491ac7c0 feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:03 +08:00
xream
78e3024cec feat: Node.js 前端代理后端路由 2023-12-12 22:52:50 +08:00
xream
5e21a20e37 fix: 修复 Loon Trojan WS 传输层 2023-12-12 21:13:17 +08:00
xream
76b5dc5809 feat: 脚本筛选支持节点快捷脚本. 语法与 Shadowrocket 脚本类似
```
const port = Number($server.port)

return [80, 443].includes(port)
```
2023-12-11 11:57:12 +08:00
xream
a1776644a0 feat: Node 版后端支持挂载前端文件夹, 环境变量 SUB_STORE_FRONTEND_PATH, SUB_STORE_FRONTEND_HOST, SUB_STORE_FRONTEND_PORT 2023-12-10 13:13:39 +08:00
xream
7aaa03d4ca chore: workflow 2023-12-10 09:32:56 +08:00
xream
d0cba285ab fix: 处理 Hysteria2 URI 中的密码部分 2023-12-09 02:08:59 +08:00
xream
d636e1b94c fix: 处理预览时子订阅出错的情况 2023-12-08 18:16:50 +08:00
xream
69726cd5c4 fix: 处理 IPv6 地址 URI 2023-12-08 17:53:07 +08:00
xream
8918479b9e release: backend version 2.14.114 2023-12-08 11:49:11 +08:00
xream
17504ab5aa Merge pull request #261 from Ariesly/master 2023-12-08 11:45:58 +08:00
Ariesly
0d8fa91cd5 fix(hysteria2): For shadowrocket obfs 2023-12-08 01:51:54 +00:00
Ariesly
e7dfa1ce38 chore(hysteria2): Uri support with tfo 2023-12-08 01:34:53 +00:00
Ariesly
fe937d6ebf fix(hysteria2): Change to TLS Fingerprint 2023-12-08 01:30:09 +00:00
xream
b7b734f529 release: backend version 2.14.113 2023-12-07 18:15:21 +08:00
xream
f5ef6010bc Merge pull request #260 from Ariesly/master
feat: Hysteria2 URI
2023-12-07 18:03:26 +08:00
Ariesly
0e82a7669d feat: Hysteria2 URI 2023-12-07 06:25:33 +00:00
xream
6d11ea0fcc feat: ProxyUtils.produce 增加第二个参数 type, 暂时仅支持目标为 ClashMetainternal 输出节点数组供开发者使用 2023-12-05 21:53:22 +08:00
xream
75f802f607 fix: 默认 User-Agent 改为 clash.meta 后, 调整订阅预处理器的逻辑, 减少 Base64 误判 2023-12-05 12:43:13 +08:00
xream
000e90d114 feat: 手动下载备份文件和使用备份上传恢复(前端版本 > 2.14.15) 2023-12-04 16:07:10 +08:00
xream
c2499f6779 fix: 修复 Base64 内容的判断 2023-12-02 16:14:11 +08:00
xream
bf9210fc5a fix: 修复多行订阅流量(仅传递首个订阅的流量信息) 2023-12-01 17:09:56 +08:00
xream
53dd1fd4c5 feat: 支持不规范的 Loon ss+simple obfs 协议格式 2023-11-30 16:01:13 +08:00
xream
c541b83037 feat: 支持按顺序合并本地和远程订阅(前端版本 > 2.14.14 可输入) 2023-11-29 03:57:20 +08:00
xream
3054d5cd5d feat: 远程订阅支持换行符连接的多个订阅链接(前端版本 > 2.14.13 可输入) 2023-11-29 02:24:03 +08:00
xream
5a645081d1 fix: SS URI 端口取整数部分 2023-11-28 23:14:45 +08:00
xream
1fc5b764fe feat: 支持设置默认 User-Agent 2023-11-25 04:31:17 +08:00
xream
5f1415d9d4 feat: 后端支持自定义 hostport. 环境变量 SUB_STORE_BACKEND_API_HOST 默认 ::, SUB_STORE_BACKEND_API_PORT 默认 3000 2023-11-24 18:31:13 +08:00
xream
1e3b4a147a feat: 增加了节点字段 1. no-resolve, 可用于跳过域名解析 2. resolved 用来标记域名解析是否成功 2023-11-21 20:10:05 +08:00
xream
905a50c0b9 fix: Hysteria/Hysteria2 输出到 Stash 时 down-speed 和 up-speed 字段截取数字部分 2023-11-20 11:22:01 +08:00
xream
89e8a99729 Merge pull request #250 from YES-Lee/patch-1
feat: add sync task for qx
2023-11-19 11:44:04 +08:00
xream
ff8573cae7 fix: 修复 app 版参数 2023-11-16 12:49:06 +08:00
xream
1ae1ec40ca feat: 补全 Surge 全协议的 no-error-alert 和 ip-version 字段 2023-11-15 15:16:34 +08:00
xream
53925518b4 feat: Sub-Store 生成的订阅地址支持传入 订阅链接/User-Agent/节点内容 可以复用此订阅的其他设置
例如: 建一个 name 为 sub 的订阅, 配置好节点操作

以后可以自由传入参数 无需在 Sub-Store 前端创建新的配置

`/download/sub?target=Surge&content=encodeURIComponent编码过的本地节点`

`/download/sub?target=Surge&url=encodeURIComponent编码过的订阅链接&ua=encodeURIComponent编码过的User-Agent`
2023-11-14 21:46:56 +08:00
xream
f3de132d70 feat: 脚本链接的末尾加上 #noCache 关闭缓存 2023-11-14 21:14:47 +08:00
xream
3e30a35bc4 feat: 脚本操作支持节点快捷脚本. 语法与 Shadowrocket 脚本类似
```
$server.name = '前缀-' + $server.name
$server.ecn = true
$server['test-url'] = 'http://1.0.0.1/generate_204'
```
2023-11-14 17:07:01 +08:00
xream
3e5f3eafdd feat: 脚本操作 ProxyUtils 增加了 isIPv4, isIPv6, isIP 方法 2023-11-14 00:57:52 +08:00
xream
9c78b87834 feat: 兼容某些格式的 Trojan URI(首个 # 之后的字符串均视为节点名称) 2023-11-13 18:49:50 +08:00
xream
ea88cc1794 feat: 支持 QX tls-pubkey-sha256 tls-alpn tls-no-session-ticket tls-no-session-reuse 字段 2023-11-13 14:34:36 +08:00
xream
c8b197c0a1 feat: 支持 QX server_check_url 和 Stash benchmark-url 字段 2023-11-13 14:06:44 +08:00
xream
69fab11344 feat: 兼容传输层 headers 中小写的 host 字段 2023-11-08 09:54:53 +08:00
xream
955c74a77d feat: 兼容某些机场订阅 hysteria 节点中的 auth_str 字段(将会在未来某个时候删除 但是有的机场不规范) 2023-11-08 07:44:12 +08:00
xream
6d51774d36 feat: 为脚本操作增加流量信息操作 flowUtils 2023-11-07 16:42:28 +08:00
xream
a91f9d7728 feat: 兼容另一种 username password 格式 2023-10-31 21:59:34 +08:00
xream
df366cf8eb doc: pnpm 2023-10-30 01:44:18 +08:00
xream
c547f34f57 feat: 支持 Loon Hysteria2(ecn, 流量控制参数未知) 2023-10-29 23:04:56 +08:00
xream
a4ff32331a fix: 简单限制一下订阅/组合订阅的名称(不可包含 "/" ) 2023-10-29 22:38:20 +08:00
xream
14648d6401 feat: 订阅链接支持参数(例: https://foo.com#noCache 关闭缓存) 2023-10-26 11:26:31 +08:00
Johnson
6216217286 feat: add qx sync task 2023-10-25 10:44:22 -05:00
xream
6a66475154 feat: Surge 支持 block-quic 参数 2023-10-24 09:31:48 +08:00
xream
adc95bba60 feat: Surge 全协议支持 Shadow TLS, 部分协议增加 TLS Fingerprint 支持 2023-10-24 07:26:34 +08:00
xream
fab3644b86 feat: 支持 Shadowrocket Hysteria2 URI 格式输入 2023-10-18 23:48:45 +08:00
xream
c21ce0be16 fix: Surge Hysteria2 输出重复添加 tfo 的 bug 2023-10-18 05:09:10 +08:00
xream
fa65eb1850 feat: Base64 订阅关键词增加 VLESS 和 Hysteria2 2023-10-16 22:11:26 +08:00
xream
79c9b89c5f feat: Stash Hysteria2 2023-10-15 15:55:19 +08:00
xream
fca508ba8a feat: Surge Hysteria2 输入/输出增加 ecn 参数 2023-10-12 22:15:10 +08:00
xream
21b531a44d feat: Surge TUIC 输入/输出增加 ecn 参数 2023-10-12 22:09:58 +08:00
xream
4e5b46a43d feat: Surge Hysteria2 输出增加 download-bandwidth(若有值但解析失败则为 0) 2023-10-12 00:39:10 +08:00
xream
bf81ca4acf feat: 输入增加 Hysteria2 URI 支持; Surge Hysteria2 输出增加 fingerprint 2023-10-11 23:35:42 +08:00
xream
e7c0b23222 feat: Surge 输入输出增加 Hysteria2 2023-10-09 23:42:22 +08:00
xream
40fb0fd7f3 feat: 兼容更多 VMess URI 格式 2023-10-09 17:36:11 +08:00
xream
b061fca356 feat: Surge Snell 输入支持解析 reuse 字段 2023-10-08 16:42:35 +08:00
xream
d3c6c99b0a feat: proxy 增加 subName(订阅名), collectionName(组合订阅名); 脚本增加第三个参数 env(包含订阅/组合订阅/环境/版本等信息) 2023-10-08 13:21:22 +08:00
xream
3fbc280e28 [+] 重复节点通知中增加订阅名称和重复节点名称 2023-10-02 16:21:08 +08:00
xream
9e3e4c6e46 [+] Surge 输出支持 underlying-proxy; VMess/Vless URI 支持 gRPC mode(默认为 gun) 2023-10-01 22:05:51 +08:00
xream
bc0dd4b175 feat: 支持 hysteria2 2023-09-22 14:43:43 +08:00
xream
7603fac036 fix: 修复部分环境无 clearTimeout 的问题 2023-09-18 20:09:03 +08:00
K
9acc161684 fix @ 2023-09-15 18:52:21 +08:00
xream
024582a99d fix: 修复 sub-store-0 路由 2023-09-15 18:42:53 +08:00
xream
1d31a80b9f fix: 修复文件和模块命名/重复添加的逻辑 2023-09-15 10:08:36 +08:00
xream
b2d0276836 feat: 文件和模块接口获取原始内容; 文件列表不返回原始内容 2023-09-14 18:51:23 +08:00
xream
3211fbf357 feat: 模块接口; 脚本参数支持 JSON 和 URL编码 2023-09-14 17:34:24 +08:00
xream
33a17c2d66 feat: 实验性支持本地脚本复用 2023-09-14 08:56:33 +08:00
xream
2c89a0ddbd feat: 支持 Clash VLESS 输出(与 Clash.Meta 的区别为: 无 XTLS 2023-09-11 02:35:36 +08:00
xream
939022e5a3 fix: 修复了 Clash.Meta 输出 VLESS 时 内部字段 sni 未作用到 servername 的问题 2023-09-09 14:03:40 +08:00
xream
59bca5670d fix: 预览时脚本下载报错导致的崩溃 2023-09-07 23:17:36 +08:00
Peng-YM
07b38cf971 release: backend version 2.14.49 2023-09-04 23:16:52 +08:00
Peng-YM
28186f596f feat: added the ability to change the base path for the data files
before starting node, use the command `export SUB_STORE_DATA_BASE_PATH="<YOUR_PATH>"`
2023-09-04 23:16:13 +08:00
xream
ea31b1d0ec fix: 排序接口修正为使用 name 排序 2023-09-04 21:31:55 +08:00
xream
77191f9caa feat: 为 Gist 备份还原增加基础校验逻辑 2023-09-04 17:06:37 +08:00
xream
07a270963e feat: 支持 Surge WireGuard 的输入和输出(由于 Surge 配置的特殊性, 仅支持 同进同出) 支持的字段格式: HK WARP = wireguard, section-name=Cloudflare, no-error-alert=true, underlying-proxy=HK, test-url=http://1.0.0.1/generate_204, ip-version=v4-only 2023-09-01 02:44:43 +08:00
xream
f1e1d50a2c fix: 暂时将后端上传限制放宽到 1mb 2023-09-01 02:07:24 +08:00
Peng-YM
a65cd1f1c9 Update README.md 2023-08-31 16:41:50 +08:00
xream
5b0e2e1ef2 docs: config 2023-08-30 22:52:05 +08:00
xream
b29266ac57 chore: sync to GitLab 2023-08-30 16:19:17 +08:00
xream
336ddd6706 chore: 调整部分日志 2023-08-29 13:52:02 +08:00
xream
25ec219659 docs: 更新 Surge SSR 协议说明; 模块说明页增加更新说明的链接 2023-08-29 01:59:01 +08:00
xream
41d24b131a feat: 根据 UA 识别 macOS 版 Surge(也可指定参数 target=SurgeMac) 并支持 SSR 协议(节点字段 exec 为 ssr-local 路径, 默认 /usr/local/bin/ssr-local; 端口从 10000 开始递增, 暂不支持配置) 2023-08-29 01:46:49 +08:00
xream
ba78982f41 feat: 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组 2023-08-28 23:47:10 +08:00
xream
26193301b3 fix: 仅在 VMess/VLESS 且传输层为 http 时设置 Host 为数组 2023-08-28 23:38:03 +08:00
xream
0141e48200 feat: 增加还原备份完成的日志输出 2023-08-28 23:29:53 +08:00
xream
5ae6687b1f chore: changelog 2023-08-28 23:15:48 +08:00
xream
ad6d1ab441 fix: build dist 2023-08-28 20:41:40 +08:00
xream
f5aea14904 fix: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host) 2023-08-28 20:34:22 +08:00
Peng-YM
4f2c95f6ab chore: remove unnecessary files 2023-08-28 20:07:32 +08:00
Peng-YM
be1e2c9979 chore: removed tracking dist files from git 2023-08-28 20:06:50 +08:00
Peng-YM
347b19e30d remove: deprecated artifact 2023-08-28 20:01:05 +08:00
xream
f94a12bf6e feat: bundle 2023-08-28 19:01:34 +08:00
xream
bd510a9aa9 fix: sync 2023-08-28 18:48:33 +08:00
xream
f02af9d643 fix: vless servername 2023-08-28 15:32:08 +08:00
xream
af8e965866 feat: new target platform "Clash.Meta" 2023-08-28 13:10:48 +08:00
xream
4bebcff1d3 feat: 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名 2023-08-28 00:09:24 +08:00
xream
7b8f6f7e52 feat: 域名解析新增 Tencent, Ali; 脚本下载失败, 脚本操作失败, 脚本过滤失败时都会报错了 2023-08-27 23:17:57 +08:00
xream
49c7107d20 fix: transport headers may have no Host 2023-08-27 18:17:30 +08:00
xream
8bfa4dbbf2 feat: VLESS URI 2023-08-27 00:57:21 +08:00
xream
5e14d05c30 feat: 组合订阅错误信息将包含出现错误的子订阅名称; 获取流量失败时, 不影响节点订阅; 订阅上游无有效节点时将报错 2023-08-26 20:27:12 +08:00
xream
8c5dca71fb feat: Loon WireGuard 2023-08-26 15:00:46 +08:00
xream
4973454f58 feat: wireguard 2023-08-25 22:48:03 +08:00
xream
4c6ba2cdc8 feat: hysteria 2023-08-25 16:19:08 +08:00
Hsiaoyi
9cbbd0e86f Merge pull request #233 from eltociear/master-1
Fix typo in README.md
2023-08-24 21:46:03 +08:00
xream
0320a77451 feat: producers adjustments, VMess URI formats 2023-08-24 21:43:58 +08:00
xream
afb9296158 feat: Added support for VMess URI in other formats and VMess without transport settings 2023-08-24 20:23:48 +08:00
xream
9b0c15ebc2 fix: 兼容 value 为空的 Trojan URI 2023-08-24 11:38:27 +08:00
xream
46738d5947 fix: trojan network tcp 2023-08-24 11:08:43 +08:00
xream
1f505752ae fix: trojan uri and tls 2023-08-24 10:02:03 +08:00
Ikko Eltociear Ashimine
0734a3d563 Fix typo in README.md
Speicial -> Special
2023-08-24 00:46:24 +09:00
xream
497bc264e3 fix: servername/sni priority over wss host 2023-08-22 18:21:34 +08:00
xream
feb207b333 fix: servername/sni priority over wss host 2023-08-22 17:28:39 +08:00
xream
9ac1112b37 fix: VMess URI alterId parseInt 2023-08-22 15:29:55 +08:00
xream
96769598ef fix: QX tls 2023-08-22 00:42:53 +08:00
xream
f8ed6a3342 fix: QX tls 2023-08-22 00:08:53 +08:00
xream
99b19c410d fix: vmess/vless http-opts.path/http-opts.headers.Host must be an array in some clients 2023-08-21 22:16:07 +08:00
xream
9e54507bbb fix: double quotes in Surge vmess ws-headers Host 2023-08-21 21:20:31 +08:00
xream
20afa0ad22 Surge 默认模块不带 ability 参数; 分离出固定带参和不带参的模块 2023-08-20 17:22:51 +08:00
walkxspace
c5b6960b35 Update geo.js (#231) 2023-08-19 11:44:55 +08:00
xream
4dd86cb368 feat: Added replaceArtifact API 2023-08-18 13:48:37 +08:00
xream
4a0319e95f fix: flexible cipher for Loon 2023-08-15 21:22:33 +08:00
xream
090d8a978f feat: Added support for scy of VMESS URI 2023-08-15 18:15:04 +08:00
xream
bc9fae6062 feat: Added support for SNI & allowInsecure of Trojan URI 2023-08-15 17:25:25 +08:00
xream
048344268c feat: Added replaceSubscriptions, replaceCollection API 2023-08-15 15:48:57 +08:00
xream
c5746f6a6b Fixed: fast-open tfo 2023-08-15 14:59:27 +08:00
xream
5cb226da62 feat: Added support for SS URI in other formats 2023-08-15 01:48:54 +08:00
xream
d229047744 Fixed: unsupported cipher for Clash/Stash 2023-08-14 10:04:47 +08:00
Hsiaoyi
cb21a8e6ec Merge pull request #229 from xream/feature/tuic
Adjust the logic for determining the tuic version
2023-08-13 17:03:40 +08:00
xream
537a00e8a9 Adjust the logic for determining the tuic version 2023-08-13 17:00:44 +08:00
Hsiaoyi
b770578cba Merge pull request #228 from xream/feature/tuic
feat: Added support for tuic and some compatibility adjustments
2023-08-13 15:56:52 +08:00
xream
47a95e5a3d feat: Added support for tuic and some compatibility adjustments 2023-08-13 15:54:04 +08:00
Hsiaoyi
e99f13d487 Merge pull request #227 from xream/feature/snell
feat: Added support for producing snell nodes with reuse and optional obfs
2023-07-31 18:44:53 +08:00
xream
fcab8401e0 feat: Added support for producing snell nodes with reuse and optional obfs 2023-07-31 18:41:48 +08:00
Hsiaoyi
431b1a3c8e Merge pull request #226 from Keywos/master
fixed deleted gist
2023-07-31 17:42:49 +08:00
Hsiaoyi
36d46003d6 Fixed: empty uploading files 2023-07-31 17:42:31 +08:00
K
ff71f12996 version 2.14.3 2023-07-31 16:41:22 +08:00
K
f7c08e3a56 fixed deleted gist 2023-07-31 16:38:07 +08:00
K
6eea8bb2d0 Merge branch 'sub-store-org:master' into master 2023-07-31 14:58:54 +08:00
Hsiaoyi
fc90e22a48 Added Surge-Noability.sgmodule 2023-07-28 22:38:36 +08:00
Hsiaoyi
26d47b019b Merge pull request #223 from xream/feature/V2Ray
feat: V2Ray Producer
Fixes #180
2023-07-26 09:55:45 +08:00
xream
8e49a78f45 feat: V2Ray Producer 2023-07-26 09:48:14 +08:00
Hsiaoyi
edee10cee3 Update Surge.sgmodule 2023-07-26 09:03:59 +08:00
K
20d958d74f Update Surge.sgmodule 2023-07-26 01:48:47 +08:00
Hsiaoyi
6427f99545 Update Surge.sgmodule
ability=http-client-policy
2023-07-24 14:39:21 +08:00
Hsiaoyi
7d2ea10206 Merge pull request #219 from Keywos/script-Cache
surge
2023-07-23 18:00:42 +08:00
Hsiaoyi
e862235cb8 Merge pull request #220 from xream/fix/FullConfig
fix: Full Config Preprocessor
2023-07-23 17:42:06 +08:00
xream
38f1728e42 fix: Full Config Preprocessor 2023-07-23 17:38:29 +08:00
K
d963be87f8 [!] Surge 2023-07-23 15:11:32 +08:00
K
390e4540d2 Merge branch 'script-Cache' of https://github.com/Keywos/Sub-Store into script-Cache 2023-07-22 15:24:20 +08:00
K
0bd00406f3 [-] log 2023-07-22 15:24:19 +08:00
Hsiaoyi
b9ce4e8f20 Merge pull request #218 from sub-store-org/dependabot/npm_and_yarn/backend/axios-0.21.2
build(deps-dev): bump axios from 0.20.0 to 0.21.2 in /backend
2023-07-22 14:35:52 +08:00
Hsiaoyi
de15bbf3ea using Node.js v16 2023-07-22 14:34:06 +08:00
dependabot[bot]
5d6bd1415b build(deps-dev): bump axios from 0.20.0 to 0.21.2 in /backend
Bumps [axios](https://github.com/axios/axios) from 0.20.0 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.2/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.20.0...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-22 06:17:45 +00:00
Hsiaoyi
6e9c3ead4c Merge pull request #217 from Keywos/script-Cache
[+] version 2.14.0
2023-07-22 14:15:56 +08:00
K
b3ccd5743a Merge branch 'sub-store-org:master' into script-Cache 2023-07-22 14:13:09 +08:00
K
e18c215fe4 [+] version 2023-07-22 14:11:45 +08:00
Hsiaoyi
e4b54b43a1 Merge pull request #216 from Keywos/script-Cache
script-Cache
2023-07-22 13:56:54 +08:00
K
21726bf950 script-Cache 2023-07-22 13:53:47 +08:00
Hsiaoyi
f6ca9af00f fix: tasks cache in Node.js environment (#209) 2023-05-09 17:16:35 +08:00
Hsiaoyi
39b79b6ca4 feat: Added support for producing Surge nodes with test-url (#199) 2023-03-19 18:32:34 +08:00
72 changed files with 7721 additions and 3793 deletions

View File

@@ -4,12 +4,12 @@ on:
branches: branches:
- master - master
paths: paths:
- 'backend/package.json' - "backend/package.json"
pull_request: pull_request:
branches: branches:
- master - master
paths: paths:
- 'backend/package.json' - "backend/package.json"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -17,11 +17,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: 'master' ref: "master"
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: "14" node-version: "16"
- name: Install dependencies - name: Install dependencies
run: | run: |
npm install -g pnpm npm install -g pnpm
@@ -34,21 +34,34 @@ jobs:
run: | run: |
cd backend cd backend
pnpm run build pnpm run build
- name: Bundle
run: |
cd backend
pnpm i -D estrella
pnpm run bundle
- id: tag - id: tag
name: Generate release tag name: Generate release tag
run: | run: |
cd backend cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"` SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "::set-output name=release_tag::$SUBSTORE_RELEASE" echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ success() }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ steps.tag.outputs.release_tag }} tag_name: ${{ steps.tag.outputs.release_tag }}
generate_release_notes: true
files: | files: |
./backend/sub-store.min.js ./backend/sub-store.min.js
./backend/dist/sub-store-0.min.js ./backend/dist/sub-store-0.min.js
./backend/dist/sub-store-1.min.js ./backend/dist/sub-store-1.min.js
./backend/dist/sub-store-parser.loon.min.js ./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js ./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

7
.gitignore vendored
View File

@@ -128,3 +128,10 @@ out
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Dist files
backend/dist/*
!backend/dist/.gitkeep
backend/sub-store.min.js
CHANGELOG.md

View File

@@ -7,10 +7,10 @@
</div> </div>
<p align="center" color="#6a737d"> <p align="center" color="#6a737d">
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket. Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
</p> </p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/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/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/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)
[!["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)
@@ -20,6 +20,8 @@ Core functionalities:
2. Subscription formatting. 2. Subscription formatting.
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.
## 1. Subscription Conversion ## 1. Subscription Conversion
### Supported Input Formats ### Supported Input Formats
@@ -28,18 +30,29 @@ Core functionalities:
- [x] SSR URI - [x] SSR URI
- [x] SSD URI - [x] SSD URI
- [x] V2RayN URI - [x] V2RayN URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP) - [x] Hysteria2 URI
- [x] Loon (SS, SSR, VMess, Trojan, HTTP) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5)
- [x] Surge (SS, VMess, Trojan, HTTP) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria2)
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP) - [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, TUIC, Snell, Hysteria2, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria2, TUIC)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms
- [x] QX - [x] QX
- [x] Loon - [x] Loon
- [x] Surge - [x] Surge
- [x] Stash & Clash - [x] Surfboard
- [x] ShadowRocket - [x] Stash
- [x] Clash.Meta
- [x] Clash
- [x] Shadowrocket
- [x] V2Ray
- [x] V2Ray URI
- [x] Plain JSON
## 2. Subscription Formatting ## 2. Subscription Formatting
@@ -61,25 +74,22 @@ Core functionalities:
- [x] **Regex rename operator**: replace by regex in proxy names. - [x] **Regex rename operator**: replace by regex in proxy names.
- [x] **Regex delete operator**: delete by regex in proxy names. - [x] **Regex delete operator**: delete by regex in proxy names.
- [x] **Script operator**: modify proxy by script. - [x] **Script operator**: modify proxy by script.
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
### Development ### Development
Go to `backend` and `web` directories, install node dependencies: Install `pnpm`
Go to `backend` directories, install node dependencies:
``` ```
npm install pnpm install
``` ```
1. In `backend`, run the backend server on http://localhost:3000 1. In `backend`, run the backend server on http://localhost:3000
``` ```
npm run serve pnpm start
```
2. In`web`, start the vue-cli server
```
npm start
``` ```
## LICENSE ## LICENSE
@@ -88,7 +98,12 @@ This project is under the GPL V3 LICENSE.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
## Star History
[![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!
- Speicial thanks to @Orz-3 and @58xinian for their awesome icons. - Special thanks to @Orz-3 and @58xinian for their awesome icons.

View File

@@ -9,7 +9,7 @@
* @updated: <%= updated %> * @updated: <%= updated %>
* @version: <%= pkg.version %> * @version: <%= pkg.version %>
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */

25
backend/bundle.js Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { build } = require('esbuild');
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
encoding: 'utf8',
});
content = content.replace(
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
'$2',
);
fs.writeFileSync(path.join(__dirname, 'dist/sub-store.no-bundle.js'), content, {
encoding: 'utf8',
});
build({
entryPoints: ['dist/sub-store.no-bundle.js'],
bundle: true,
minify: true,
sourcemap: true,
platform: 'node',
format: 'cjs',
outfile: 'dist/sub-store.bundle.js',
});

0
backend/dist/.gitkeep vendored Normal file
View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.13.6", "version": "2.14.143",
"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": {
@@ -8,14 +8,17 @@
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive", "test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
"serve": "node sub-store.min.js", "serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js", "start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"build": "gulp" "build": "gulp",
"bundle": "node bundle.js"
}, },
"author": "Peng-YM", "author": "Peng-YM",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"automerge": "1.0.1-preview.7", "automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"connect-history-api-fallback": "^2.0.0",
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"request": "^2.88.2", "request": "^2.88.2",
@@ -30,12 +33,13 @@
"@babel/preset-env": "^7.18.0", "@babel/preset-env": "^7.18.0",
"@babel/register": "^7.17.7", "@babel/register": "^7.17.7",
"@types/gulp": "^4.0.9", "@types/gulp": "^4.0.9",
"axios": "^0.20.0", "axios": "^0.21.2",
"babel-plugin-relative-path-import": "^2.0.1", "babel-plugin-relative-path-import": "^2.0.1",
"babelify": "^10.0.0", "babelify": "^10.0.0",
"browser-pack-flat": "^3.4.2", "browser-pack-flat": "^3.4.2",
"browserify": "^17.0.0", "browserify": "^17.0.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"esbuild": "^0.19.8",
"eslint": "^8.16.0", "eslint": "^8.16.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-babel": "^8.0.0", "gulp-babel": "^8.0.0",

6818
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
export const SETTINGS_KEY = 'settings'; export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs'; export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections'; export const COLLECTIONS_KEY = 'collections';
export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts'; export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules'; export const RULES_KEY = 'rules';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup'; export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
@@ -9,3 +11,5 @@ export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository'; export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource'; export const RESOURCE_CACHE_KEY = '#sub-store-cached-resource';
export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour export const CACHE_EXPIRATION_TIME_MS = 60 * 60 * 1000; // 1 hour
export const SCRIPT_RESOURCE_CACHE_KEY = '#sub-store-cached-script-resource'; // cached-script-resource CSR
export const CSR_EXPIRATION_TIME_KEY = '#sub-store-csr-expiration-time'; // Custom expiration time key; (Loon|Surge) Default write 48 hour

View File

@@ -1,5 +1,5 @@
import download from '@/utils/download'; import download from '@/utils/download';
import { isIPv4, isIPv6, isValidPortNumber } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors'; import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers'; import PROXY_PRODUCERS from './producers';
@@ -36,7 +36,7 @@ function parse(raw) {
if (lastParser) { if (lastParser) {
const [proxy, error] = tryParse(lastParser, line); const [proxy, error] = tryParse(lastParser, line);
if (!error) { if (!error) {
proxies.push(proxy); proxies.push(lastParse(proxy));
success = true; success = true;
} }
} }
@@ -46,7 +46,7 @@ function parse(raw) {
for (const parser of PROXY_PARSERS) { for (const parser of PROXY_PARSERS) {
const [proxy, error] = tryParse(parser, line); const [proxy, error] = tryParse(parser, line);
if (!error) { if (!error) {
proxies.push(proxy); proxies.push(lastParse(proxy));
lastParser = parser; lastParser = parser;
success = true; success = true;
$.info(`${parser.name} is activated`); $.info(`${parser.name} is activated`);
@@ -63,35 +63,50 @@ function parse(raw) {
return proxies; return proxies;
} }
async function process(proxies, operators = [], targetPlatform) { async function process(proxies, operators = [], targetPlatform, source) {
for (const item of operators) { for (const item of operators) {
// process script // process script
let script; let script;
const $arguments = {}; let $arguments = {};
if (item.type.indexOf('Script') !== -1) { if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args; const { mode, content } = item.args;
if (mode === 'link') { if (mode === 'link') {
const url = content; let noCache;
let url = content;
if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, '');
noCache = true;
}
// extract link arguments // extract link arguments
const rawArgs = url.split('#'); const rawArgs = url.split('#');
if (rawArgs.length > 1) { if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) { for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0]; const key = pair.split('=')[0];
const value = pair.split('=')[1] || true; const value = pair.split('=')[1];
$arguments[key] = value; // 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
} }
} }
// if this is a remote script, download it // if this is a remote script, download it
try { try {
script = await download(url.split('#')[0]); script = await download(
`${url.split('#')[0]}${noCache ? '#noCache' : ''}`,
);
// $.info(`Script loaded: >>>\n ${script}`); // $.info(`Script loaded: >>>\n ${script}`);
} catch (err) { } catch (err) {
$.error( $.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`, `Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
); );
// skip the script if download failed. throw new Error(`无法下载脚本: ${url}`);
continue;
} }
} else { } else {
script = content; script = content;
@@ -114,6 +129,7 @@ async function process(proxies, operators = [], targetPlatform) {
script, script,
targetPlatform, targetPlatform,
$arguments, $arguments,
source,
); );
} else { } else {
processor = PROXY_PROCESSORS[item.type](item.args || {}); processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -123,7 +139,7 @@ async function process(proxies, operators = [], targetPlatform) {
return proxies; return proxies;
} }
function produce(proxies, targetPlatform) { function produce(proxies, targetPlatform, type) {
const producer = PROXY_PRODUCERS[targetPlatform]; const producer = PROXY_PRODUCERS[targetPlatform];
if (!producer) { if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`); throw new Error(`Target platform: ${targetPlatform} is not supported!`);
@@ -137,10 +153,21 @@ function produce(proxies, targetPlatform) {
$.info(`Producing proxies for target: ${targetPlatform}`); $.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
return proxies return proxies
.map((proxy) => { .map((proxy) => {
try { try {
return producer.produce(proxy); let line = producer.produce(proxy, type);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
} catch (err) { } catch (err) {
$.error( $.error(
`Cannot produce proxy: ${JSON.stringify( `Cannot produce proxy: ${JSON.stringify(
@@ -155,7 +182,7 @@ function produce(proxies, targetPlatform) {
.filter((line) => line.length > 0) .filter((line) => line.length > 0)
.join('\n'); .join('\n');
} else if (producer.type === 'ALL') { } else if (producer.type === 'ALL') {
return producer.produce(proxies); return producer.produce(proxies, type);
} }
} }
@@ -163,6 +190,9 @@ export const ProxyUtils = {
parse, parse,
process, process,
produce, produce,
isIPv4,
isIPv6,
isIP,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -182,3 +212,79 @@ function safeMatch(parser, line) {
return false; return false;
} }
} }
function lastParse(proxy) {
if (isValidPortNumber(proxy.port)) {
proxy.port = parseInt(proxy.port, 10);
}
if (proxy.server) {
proxy.server = proxy.server
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '');
}
if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') {
delete proxy.network;
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
proxy.tls = true;
}
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
if (transporthost && !transportHost) {
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
delete proxy[`${proxy.network}-opts`].headers.host;
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
if (transportHost) {
proxy.sni = transportHost;
}
}
if (!proxy.sni && !isIP(proxy.server)) {
proxy.sni = proxy.server;
}
}
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
if (
!proxy.tls &&
['ws', 'http'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
!isIP(proxy.server)
) {
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].headers =
proxy[`${proxy.network}-opts`].headers || {};
proxy[`${proxy.network}-opts`].headers.Host =
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
? [proxy.server]
: proxy.server;
}
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
if (transportHost && !Array.isArray(transportHost)) {
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
}
if (transportPath && !Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = [transportPath];
}
}
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
delete proxy.ports;
}
return proxy;
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}

View File

@@ -23,12 +23,21 @@ function URI_SS() {
}; };
content = content.split('#')[0]; // strip proxy name content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6 // handle IPV4 and IPV6
const serverAndPort = content.match(/@([^/]*)(\/|$)/)[1]; let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]);
if (!serverAndPortArray) {
content = Base64.decode(content);
userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
}
const serverAndPort = serverAndPortArray[1];
const portIdx = serverAndPort.lastIndexOf(':'); const portIdx = serverAndPort.lastIndexOf(':');
proxy.server = serverAndPort.substring(0, portIdx); proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = serverAndPort.substring(portIdx + 1); proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
/\d+/,
)?.[0];
const userInfo = Base64.decode(content.split('@')[0]).split(':'); const userInfo = userInfoStr.split(':');
proxy.cipher = userInfo[0]; proxy.cipher = userInfo[0];
proxy.password = userInfo[1]; proxy.password = userInfo[1];
@@ -150,7 +159,7 @@ function URI_VMess() {
}; };
const parse = (line) => { const parse = (line) => {
line = line.split('vmess://')[1]; line = line.split('vmess://')[1];
const content = Base64.decode(line); let content = Base64.decode(line);
if (/=\s*vmess/.test(content)) { if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format // Quantumult VMess URI format
const partitions = content.split(',').map((p) => p.trim()); const partitions = content.split(',').map((p) => p.trim());
@@ -202,30 +211,111 @@ function URI_VMess() {
} }
return proxy; return proxy;
} else { } else {
let params = {};
try {
// V2rayN URI format // V2rayN URI format
const params = JSON.parse(content); params = JSON.parse(content);
} catch (e) {
// Shadowrocket URI format
// eslint-disable-next-line no-unused-vars
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
content = Base64.decode(base64Line);
for (const addon of qs.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
if (value.indexOf(',') === -1) {
params[key] = value;
} else {
params[key] = value.split(',');
}
}
// eslint-disable-next-line no-unused-vars
let [___, cipher, uuid, server, port] =
/(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
params.scy = cipher;
params.id = uuid;
params.port = port;
params.add = server;
}
const proxy = { const proxy = {
name: params.ps, name: params.ps ?? params.remarks,
type: 'vmess', type: 'vmess',
server: params.add, server: params.add,
port: params.port, port: parseInt(getIfPresent(params.port), 10),
cipher: 'auto', // V2rayN has no default cipher! use aes-128-gcm as default. cipher: getIfPresent(params.scy, 'auto'),
uuid: params.id, uuid: params.id,
alterId: getIfPresent(params.aid, 0), alterId: parseInt(
tls: params.tls === 'tls' || params.tls === true, getIfPresent(params.aid ?? params.alterId, 0),
10,
),
tls: ['tls', true, 1, '1'].includes(params.tls),
'skip-cert-verify': isPresent(params.verify_cert) 'skip-cert-verify': isPresent(params.verify_cert)
? !params.verify_cert ? !params.verify_cert
: undefined, : undefined,
}; };
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
proxy.sni = params.sni;
}
// handle obfs // handle obfs
if (params.net === 'ws') { if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws'; proxy.network = 'ws';
proxy['ws-opts'] = { } else if (
path: getIfNotBlank(params.path), ['tcp', 'http'].includes(params.net) ||
headers: { Host: getIfNotBlank(params.host) }, params.obfs === 'http'
) {
proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
}
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
try {
const parsedObfs = JSON.parse(transportHost);
const parsedHost = parsedObfs?.Host;
if (parsedHost) {
transportHost = parsedHost;
}
// eslint-disable-next-line no-empty
} catch (e) {}
let transportPath = params.path;
if (proxy.network === 'http') {
if (transportHost) {
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
}
if (transportPath) {
transportPath = Array.isArray(transportPath)
? transportPath[0]
: transportPath;
}
}
if (transportPath || transportHost) {
if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type),
}; };
if (proxy.tls && params.host) { } else {
proxy.sni = params.host; proxy[`${proxy.network}-opts`] = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
}
} else {
delete proxy.network;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
// sni 优先级应高于 host
if (proxy.tls && !proxy.sni && transportHost) {
proxy.sni = transportHost;
} }
} }
return proxy; return proxy;
@@ -234,6 +324,145 @@ function URI_VMess() {
return { name, test, parse }; return { name, test, parse };
} }
function URI_VLESS() {
const name = 'URI VLESS Parser';
const test = (line) => {
return /^vless:\/\//.test(line);
};
const parse = (line) => {
line = line.split('vless://')[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, ___, addons = '', name] =
/^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `VLESS ${server}:${port}`;
const proxy = {
type: 'vless',
name,
server,
port,
uuid,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.tls = params.security && params.security !== 'none';
proxy.sni = params.sni;
proxy.flow = params.flow;
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (Object.keys(opts).length > 0) {
proxy[`${params.security}-opts`] = opts;
}
}
proxy.network = params.type;
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
const opts = {};
if (params.path) {
opts.path = params.path;
}
if (params.host) {
opts.headers = { Host: params.host };
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
}
// https://github.com/XTLS/Xray-core/issues/91
if (['grpc'].includes(proxy.network)) {
opts['_grpc-type'] = params.mode || 'gun';
}
if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts;
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
}
}
return proxy;
};
return { name, test, parse };
}
function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser';
const test = (line) => {
return /^(hysteria2|hy2):\/\//.test(line);
};
const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, ____, addons = '', name] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `Hysteria2 ${server}:${port}`;
const proxy = {
type: 'hysteria2',
name,
server,
port,
password,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.sni = params.sni;
if (!proxy.sni && params.peer) {
proxy.sni = params.peer;
}
if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs;
}
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256;
return proxy;
};
return { name, test, parse };
}
// Trojan URI format // Trojan URI format
function URI_Trojan() { function URI_Trojan() {
const name = 'URI Trojan Parser'; const name = 'URI Trojan Parser';
@@ -242,8 +471,16 @@ function URI_Trojan() {
}; };
const parse = (line) => { const parse = (line) => {
let [newLine, name] = line.split(/#(.+)/, 2);
const parser = getTrojanURIParser(); const parser = getTrojanURIParser();
const proxy = parser.parse(line); const proxy = parser.parse(newLine);
if (isNotBlank(name)) {
try {
proxy.name = decodeURIComponent(name);
} catch (e) {
console.log(e);
}
}
return proxy; return proxy;
}; };
return { name, test, parse }; return { name, test, parse };
@@ -266,10 +503,15 @@ function Clash_All() {
'ss', 'ss',
'ssr', 'ssr',
'vmess', 'vmess',
'socks', 'socks5',
'http', 'http',
'snell', 'snell',
'trojan', 'trojan',
'tuic',
'vless',
'hysteria',
'hysteria2',
'wireguard',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
throw new Error( throw new Error(
@@ -278,9 +520,23 @@ function Clash_All() {
} }
// handle vmess sni // handle vmess sni
if (proxy.type === 'vmess') { if (['vmess', 'vless'].includes(proxy.type)) {
proxy.sni = proxy.servername; proxy.sni = proxy.servername;
delete proxy.servername; delete proxy.servername;
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost)
? httpHost[0]
: httpHost;
}
}
}
if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url'];
} }
return proxy; return proxy;
@@ -406,6 +662,15 @@ function Loon_Trojan() {
const parse = (line) => getLoonParser().parse(line); const parse = (line) => getLoonParser().parse(line);
return { name, test, parse }; return { name, test, parse };
} }
function Loon_Hysteria2() {
const name = 'Loon Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*Hysteria2/i.test(line.split(',')[0]);
};
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_Http() { function Loon_Http() {
const name = 'Loon HTTP Parser'; const name = 'Loon HTTP Parser';
@@ -417,6 +682,114 @@ function Loon_Http() {
return { name, test, parse }; return { name, test, parse };
} }
function Loon_WireGuard() {
const name = 'Loon WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/i.test(line.split(',')[0]);
};
const parse = (line) => {
const name = line.match(
/(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
)?.[1];
line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
let peers = line.match(
/,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
)?.[1];
let serverPort = peers.match(
/(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
);
let server = serverPort?.[2];
let port = parseInt(serverPort?.[3], 10);
let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
if (mtu) {
mtu = parseInt(mtu, 10);
}
let keepalive = line.match(
/(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
)?.[2];
if (keepalive) {
keepalive = parseInt(keepalive, 10);
}
let reserved = peers.match(
/(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
)?.[2];
if (reserved) {
reserved = JSON.parse(reserved);
}
let dns;
let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
let dnsv6 = line.match(
/(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
if (dnsv4 || dnsv6) {
dns = [];
if (dnsv4) {
dns.push(dnsv4);
}
if (dnsv6) {
dns.push(dnsv6);
}
}
let allowedIps = peers
.match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
?.split(',')
.map((i) => i.trim());
let preSharedKey = peers.match(
/(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ip = line.match(
/(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ipv6 = line.match(
/(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let publicKey = peers.match(
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
const proxy = {
type: 'wireguard',
name,
server,
port,
ip,
ipv6,
'private-key': line.match(
/(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2],
'public-key': publicKey,
mtu,
keepalive,
reserved,
'allowed-ips': allowedIps,
'preshared-key': preSharedKey,
dns,
udp: true,
peers: [
{
server,
port,
ip,
ipv6,
'public-key': publicKey,
'pre-shared-key': preSharedKey,
'allowed-ips': allowedIps,
reserved,
},
],
};
proxy;
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
proxy['remote-dns-resolve'] = true;
}
return proxy;
};
return { name, test, parse };
}
function Surge_SS() { function Surge_SS() {
const name = 'Surge SS Parser'; const name = 'Surge SS Parser';
const test = (line) => { const test = (line) => {
@@ -468,7 +841,33 @@ function Surge_Socks5() {
function Surge_Snell() { function Surge_Snell() {
const name = 'Surge Snell Parser'; const name = 'Surge Snell Parser';
const test = (line) => { const test = (line) => {
return /^.*=\s*snell?/.test(line.split(',')[0]); return /^.*=\s*snell/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_Tuic() {
const name = 'Surge Tuic Parser';
const test = (line) => {
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_WireGuard() {
const name = 'Surge WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_Hysteria2() {
const name = 'Surge Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse }; return { name, test, parse };
@@ -478,6 +877,8 @@ export default [
URI_SS(), URI_SS(),
URI_SSR(), URI_SSR(),
URI_VMess(), URI_VMess(),
URI_VLESS(),
URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
Clash_All(), Clash_All(),
Surge_SS(), Surge_SS(),
@@ -485,13 +886,18 @@ export default [
Surge_Trojan(), Surge_Trojan(),
Surge_Http(), Surge_Http(),
Surge_Snell(), Surge_Snell(),
Surge_Tuic(),
Surge_WireGuard(),
Surge_Hysteria2(),
Surge_Socks5(), Surge_Socks5(),
Loon_SS(), Loon_SS(),
Loon_SSR(), Loon_SSR(),
Loon_VMess(), Loon_VMess(),
Loon_Vless(), Loon_Vless(),
Loon_Hysteria2(),
Loon_Trojan(), Loon_Trojan(),
Loon_Http(), Loon_Http(),
Loon_WireGuard(),
QX_SS(), QX_SS(),
QX_SSR(), QX_SSR(),
QX_VMess(), QX_VMess(),

View File

@@ -35,7 +35,7 @@ const grammars = String.raw`
} }
} }
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) { start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
return proxy; return proxy;
} }
@@ -44,7 +44,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
// handle ssr obfs // handle ssr obfs
proxy.obfs = obfs.type; proxy.obfs = obfs.type;
} }
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle ss obfs // handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -68,6 +68,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
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/fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
@@ -142,6 +145,9 @@ username = & {
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
@@ -167,6 +173,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 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; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@@ -33,7 +33,7 @@
} }
} }
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) { start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
return proxy; return proxy;
} }
@@ -42,7 +42,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
// handle ssr obfs // handle ssr obfs
proxy.obfs = obfs.type; proxy.obfs = obfs.type;
} }
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle ss obfs // handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -66,6 +66,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/download_bandwidth/ecn/others)* {
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/fast_open/udp_relay/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
@@ -140,6 +143,9 @@ username = & {
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); } password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); } uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; } obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
@@ -165,6 +171,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } 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; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@@ -43,13 +43,13 @@ start = (trojan/shadowsocks/vmess/http/socks5) {
} }
trojan = "trojan" equals address trojan = "trojan" equals address
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* { (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleObfs(); handleObfs();
} }
shadowsocks = "shadowsocks" equals address shadowsocks = "shadowsocks" equals address
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* { (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
if (proxy.protocol) { if (proxy.protocol) {
proxy.type = "ssr"; proxy.type = "ssr";
// handle ssr obfs // handle ssr obfs
@@ -80,7 +80,7 @@ shadowsocks = "shadowsocks" equals address
} }
vmess = "vmess" equals address vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* { (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -92,12 +92,12 @@ vmess = "vmess" equals address
} }
http = "http" equals address http = "http" equals address
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{ (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
proxy.type = "http"; proxy.type = "http";
} }
socks5 = "socks5" equals address socks5 = "socks5" equals address
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* { (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -155,6 +155,14 @@ tls_verification = comma "tls-verification" equals flag:bool {
proxy["skip-cert-verify"] = !flag; proxy["skip-cert-verify"] = !flag;
} }
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
proxy["tls-no-session-ticket"] = flag;
}
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
proxy["tls-no-session-reuse"] = flag;
}
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; }
@@ -166,6 +174,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; } ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
uri = $[^,]+ uri = $[^,]+
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }

View File

@@ -41,13 +41,13 @@ start = (trojan/shadowsocks/vmess/http/socks5) {
} }
trojan = "trojan" equals address trojan = "trojan" equals address
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* { (password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleObfs(); handleObfs();
} }
shadowsocks = "shadowsocks" equals address shadowsocks = "shadowsocks" equals address
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* { (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* {
if (proxy.protocol) { if (proxy.protocol) {
proxy.type = "ssr"; proxy.type = "ssr";
// handle ssr obfs // handle ssr obfs
@@ -78,7 +78,7 @@ shadowsocks = "shadowsocks" equals address
} }
vmess = "vmess" equals address vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* { (uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -90,12 +90,12 @@ vmess = "vmess" equals address
} }
http = "http" equals address http = "http" equals address
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{ (username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
proxy.type = "http"; proxy.type = "http";
} }
socks5 = "socks5" equals address socks5 = "socks5" equals address
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* { (username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@@ -153,6 +153,14 @@ tls_verification = comma "tls-verification" equals flag:bool {
proxy["skip-cert-verify"] = !flag; proxy["skip-cert-verify"] = !flag;
} }
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); } tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
proxy["tls-no-session-ticket"] = flag;
}
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
proxy["tls-no-session-reuse"] = flag;
}
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; } obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; } obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; return type; }
@@ -164,6 +172,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; } ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; } ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
uri = $[^,]+ uri = $[^,]+
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); } tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }

View File

@@ -25,15 +25,18 @@ const grammars = String.raw`
proxy.network = "ws"; proxy.network = "ws";
$set(proxy, "ws-opts.path", obfs.path); $set(proxy, "ws-opts.path", obfs.path);
$set(proxy, "ws-opts.headers", obfs['ws-headers']); $set(proxy, "ws-opts.headers", obfs['ws-headers']);
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
}
} }
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -43,7 +46,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -53,18 +56,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
} }
handleWebsocket(); handleWebsocket();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
} }
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -73,10 +76,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic";
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic";
proxy.version = 5;
}
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge";
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
} }
@@ -147,6 +163,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); } passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -175,6 +192,22 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _

View File

@@ -23,15 +23,18 @@
proxy.network = "ws"; proxy.network = "ws";
$set(proxy, "ws-opts.path", obfs.path); $set(proxy, "ws-opts.path", obfs.path);
$set(proxy, "ws-opts.headers", obfs['ws-headers']); $set(proxy, "ws-opts.headers", obfs['ws-headers']);
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
}
} }
} }
} }
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) { start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2) {
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -41,7 +44,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
if (proxy.aead) { if (proxy.aead) {
@@ -51,18 +54,18 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
} }
handleWebsocket(); handleWebsocket();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
} }
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http" address (username password)? (fast_open/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@@ -71,10 +74,23 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.path", obfs.path); $set(proxy, "obfs-opts.path", obfs.path);
} }
} }
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic";
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/no_error_alert/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "tuic";
proxy.version = 5;
}
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge";
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/test_url/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "hysteria2";
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
} }
@@ -145,6 +161,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); } snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); } snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); } passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); } vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; } vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -173,6 +190,22 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _

View File

@@ -79,7 +79,7 @@ port = digits:[0-9]+ {
} }
} }
params = "?" head:param tail:("&"@param)* { params = "/"? "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"]; proxy.sni = params["sni"] || params["peer"];
@@ -88,13 +88,30 @@ params = "?" head:param tail:("&"@param)* {
$set(proxy, "ws-opts.path", params["wspath"]); $set(proxy, "ws-opts.path", params["wspath"]);
} }
if (params["type"]) {
proxy.network = params["type"]
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
}
}
proxy.udp = toBool(params["udp"]); proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]); proxy.tfo = toBool(params["tfo"]);
} }
param = kv/single; param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ { kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value; params[key] = value;
} }

View File

@@ -77,7 +77,7 @@ port = digits:[0-9]+ {
} }
} }
params = "?" head:param tail:("&"@param)* { params = "/"? "?" head:param tail:("&"@param)* {
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"]; proxy.sni = params["sni"] || params["peer"];
@@ -86,13 +86,30 @@ params = "?" head:param tail:("&"@param)* {
$set(proxy, "ws-opts.path", params["wspath"]); $set(proxy, "ws-opts.path", params["wspath"]);
} }
if (params["type"]) {
proxy.network = params["type"]
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
}
}
proxy.udp = toBool(params["udp"]); proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]); proxy.tfo = toBool(params["tfo"]);
} }
param = kv/single; param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ { kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value; params[key] = value;
} }

View File

@@ -13,17 +13,22 @@ function Base64Encoded() {
const name = 'Base64 Pre-processor'; const name = 'Base64 Pre-processor';
const keys = [ const keys = [
'dm1lc3M', 'dm1lc3M', // vmess
'c3NyOi8v', 'c3NyOi8v', // ssr://
'dHJvamFu', 'dHJvamFu', // trojan
'c3M6Ly', 'c3M6Ly', // ss:/
'c3NkOi8v', 'c3NkOi8v', // ssd://
'c2hhZG93', 'c2hhZG93', // shadow
'aHR0c', 'aHR0c', // htt
'dmxlc3M=', // vless
'aHlzdGVyaWEy', // hysteria2
]; ];
const test = function (raw) { const test = function (raw) {
return keys.some((k) => raw.indexOf(k) !== -1); return (
!/^\w+:\/\/\w+/im.test(raw) &&
keys.some((k) => raw.indexOf(k) !== -1)
);
}; };
const parse = function (raw) { const parse = function (raw) {
raw = Base64.decode(raw); raw = Base64.decode(raw);
@@ -35,7 +40,9 @@ function Base64Encoded() {
function Clash() { function Clash() {
const name = 'Clash Pre-processor'; const name = 'Clash Pre-processor';
const test = function (raw) { const test = function (raw) {
return /proxies/.test(raw); if (!/proxies/.test(raw)) return false;
const content = safeLoad(raw);
return content.proxies && Array.isArray(content.proxies);
}; };
const parse = function (raw) { const parse = function (raw) {
// Clash YAML format // Clash YAML format
@@ -95,24 +102,12 @@ function FullConfig() {
return /^(\[server_local\]|\[Proxy\])/gm.test(raw); return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
}; };
const parse = function (raw) { const parse = function (raw) {
const regex = /^\[server_local]|\[Proxy]/gm; const match = raw.match(
const match = regex.exec(raw); /^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
const results = []; )?.[1];
return match || raw;
let first = true;
if (match) {
raw = raw.substring(match.index);
for (const line of raw.split('\n')) {
if (!first && !line.test(/^\s*\[/)) {
results.push(line);
}
// skip the first line
first = false;
}
return results.join('\n');
}
}; };
return { name, test, parse }; return { name, test, parse };
} }
export default [HTML(), Base64Encoded(), Clash(), SSD(), FullConfig()]; export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];

View File

@@ -1,4 +1,5 @@
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils'; import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical'; import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo'; import { getFlag } from '@/utils/geo';
@@ -6,6 +7,8 @@ import lodash from 'lodash';
import $ from '@/core/app'; import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import env from '@/utils/env';
import { getFlowHeaders, parseFlowHeaders, flowTransfer } from '@/utils/flow';
/** /**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows: The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
@@ -293,7 +296,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) { function ScriptOperator(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -304,7 +307,24 @@ function ScriptOperator(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = operator(proxies, targetPlatform); output = operator(proxies, targetPlatform, { source, ...env });
})();
return output;
},
nodeFunc: async (proxies) => {
let output = proxies;
await (async function () {
const operator = createDynamicFunction(
'operator',
`async function operator(proxies = []) {
return proxies.map(($server = {}) => {
${script}
return $server
})
}`,
$arguments,
);
output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },
@@ -377,6 +397,46 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Ali: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain,
)}&type=A&short=1`,
headers: {
accept: 'application/dns-json',
},
});
const answers = JSON.parse(resp.body);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain) {
const id = hex_md5(`ALI:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?type=A&dn=${encodeURIComponent(
domain,
)}`,
headers: {
accept: 'application/dns-json',
},
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
}; };
function ResolveDomainOperator({ provider }) { function ResolveDomainOperator({ provider }) {
@@ -391,7 +451,9 @@ function ResolveDomainOperator({ provider }) {
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage. const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
const totalDomain = [ const totalDomain = [
...new Set( ...new Set(
proxies.filter((p) => !isIP(p.server)).map((c) => c.server), proxies
.filter((p) => !isIP(p.server) && !p['no-resolve'])
.map((c) => c.server),
), ),
]; ];
const totalBatch = Math.ceil(totalDomain.length / limit); const totalBatch = Math.ceil(totalDomain.length / limit);
@@ -415,8 +477,15 @@ function ResolveDomainOperator({ provider }) {
} }
await Promise.all(currentBatch); await Promise.all(currentBatch);
} }
proxies.forEach((proxy) => { proxies.forEach((p) => {
proxy.server = results[proxy.server] || proxy.server; if (!p['no-resolve']) {
if (results[p.server]) {
p.server = results[p.server];
p.resolved = true;
} else {
p.resolved = false;
}
}
}); });
return proxies; return proxies;
@@ -521,7 +590,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) { function ScriptFilter(script, targetPlatform, $arguments, source) {
return { return {
name: 'Script Filter', name: 'Script Filter',
func: async (proxies) => { func: async (proxies) => {
@@ -532,7 +601,23 @@ function ScriptFilter(script, targetPlatform, $arguments) {
script, script,
$arguments, $arguments,
); );
output = filter(proxies, targetPlatform); output = filter(proxies, targetPlatform, { source, ...env });
})();
return output;
},
nodeFunc: async (proxies) => {
let output = FULL(proxies.length, true);
await (async function () {
const filter = createDynamicFunction(
'filter',
`async function filter(proxies = []) {
return proxies.filter(($server = {}) => {
${script}
})
}`,
$arguments,
);
output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
return output; return output;
}, },
@@ -565,7 +650,30 @@ async function ApplyFilter(filter, objs) {
selected = await filter.func(objs); selected = await filter.func(objs);
} catch (err) { } catch (err) {
// print log and skip this filter // print log and skip this filter
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`); $.error(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
funcErr = '';
} else {
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
}
try {
selected = await filter.nodeFunc(objs);
} catch (err) {
$.error(
`Cannot apply filter ${filter.name}(node script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
if (funcErr && nodeErrMsg === funcErrMsg) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行节点快捷过滤脚本 失败 ${nodeErr}`;
}
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
}
} }
return objs.filter((_, i) => selected[i]); return objs.filter((_, i) => selected[i]);
} }
@@ -576,8 +684,33 @@ async function ApplyOperator(operator, objs) {
const output_ = await operator.func(output); const output_ = await operator.func(output);
if (output_) output = output_; if (output_) output = output_;
} catch (err) { } catch (err) {
// print log and skip this operator $.error(
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`); `Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
funcErr = '';
} else {
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
}
try {
const output_ = await operator.nodeFunc(output);
if (output_) output = output_;
} catch (err) {
$.error(
`Cannot apply operator ${operator.name}(node script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
if (funcErr && nodeErrMsg === funcErrMsg) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行节点快捷脚本 失败 ${nodeErr}`;
}
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
}
} }
return output; return output;
} }
@@ -624,6 +757,7 @@ function removeFlag(str) {
} }
function createDynamicFunction(name, script, $arguments) { function createDynamicFunction(name, script, $arguments) {
const flowUtils = { getFlowHeaders, parseFlowHeaders, flowTransfer };
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(
'$arguments', '$arguments',
@@ -633,6 +767,8 @@ function createDynamicFunction(name, script, $arguments) {
'$httpClient', '$httpClient',
'$notification', '$notification',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache',
'flowUtils',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
@@ -645,6 +781,8 @@ function createDynamicFunction(name, script, $arguments) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
$notification, $notification,
ProxyUtils, ProxyUtils,
scriptResourceCache,
flowUtils,
); );
} else { } else {
return new Function( return new Function(
@@ -652,7 +790,10 @@ function createDynamicFunction(name, script, $arguments) {
'$substore', '$substore',
'lodash', 'lodash',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache',
'flowUtils',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils); )($arguments, $, lodash, ProxyUtils, scriptResourceCache, flowUtils);
} }
} }

View File

@@ -3,12 +3,50 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() { export default function Clash_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies // filter unsupported proxies
proxies = proxies.filter((proxy) => // https://clash.wiki/configuration/outbound.html#shadowsocks
['ss', 'ssr', 'vmess', 'socks', 'http', 'snell', 'trojan'].includes( proxies = proxies.filter((proxy) => {
proxy.type, if (
), ![
); 'ss',
'ssr',
'vmess',
'vless',
'socks5',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
}
return true;
});
return ( return (
'proxies:\n' + 'proxies:\n' +
proxies proxies
@@ -25,9 +63,71 @@ export default function Clash_Producer() {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
delete proxy.sni; delete proxy.sni;
} }
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} }
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -0,0 +1,144 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies, type) => {
const list = proxies
.filter((proxy) => {
if (proxy.type === 'snell' && String(proxy.version) === '4') {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -1,9 +1,14 @@
import Surge_Producer from './surge'; import Surge_Producer from './surge';
import SurgeMac_Producer from './surgemac';
import Clash_Producer from './clash'; import Clash_Producer from './clash';
import ClashMeta_Producer from './clashmeta';
import Stash_Producer from './stash'; import Stash_Producer from './stash';
import Loon_Producer from './loon'; import Loon_Producer from './loon';
import URI_Producer from './uri'; import URI_Producer from './uri';
import V2Ray_Producer from './v2ray';
import QX_Producer from './qx'; import QX_Producer from './qx';
import ShadowRocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
function JSON_Producer() { function JSON_Producer() {
const type = 'ALL'; const type = 'ALL';
@@ -14,9 +19,14 @@ function JSON_Producer() {
export default { export default {
QX: QX_Producer(), QX: QX_Producer(),
Surge: Surge_Producer(), Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(), Loon: Loon_Producer(),
Clash: Clash_Producer(), Clash: Clash_Producer(),
ClashMeta: ClashMeta_Producer(),
URI: URI_Producer(), URI: URI_Producer(),
V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(), JSON: JSON_Producer(),
Stash: Stash_Producer(), Stash: Stash_Producer(),
ShadowRocket: ShadowRocket_Producer(),
Surfboard: Surfboard_Producer(),
}; };

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
const targetPlatform = 'Loon'; const targetPlatform = 'Loon';
import { isPresent, Result } from './utils'; import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils';
export default function Loon_Producer() { export default function Loon_Producer() {
const produce = (proxy) => { const produce = (proxy) => {
@@ -17,6 +18,10 @@ export default function Loon_Producer() {
return vless(proxy); return vless(proxy);
case 'http': case 'http':
return http(proxy); return http(proxy);
case 'wireguard':
return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -94,11 +99,11 @@ function trojan(proxy) {
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
result.append(`,transport=ws`); result.append(`,transport=ws`);
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`, `,path=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`, `,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else { } else {
@@ -127,9 +132,7 @@ function trojan(proxy) {
function vmess(proxy) { function vmess(proxy) {
const result = new Result(proxy); const result = new Result(proxy);
result.append( result.append(
`${proxy.name}=vmess,${proxy.server},${proxy.port},${ `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
proxy.cipher === 'auto' ? 'none' : proxy.cipher
},"${proxy.uuid}"`,
); );
// transport // transport
@@ -137,21 +140,23 @@ function vmess(proxy) {
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
result.append(`,transport=ws`); result.append(`,transport=ws`);
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`, `,path=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`, `,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
result.append(`,transport=http`); result.append(`,transport=http`);
let httpPath = proxy['http-opts']?.path;
let httpHost = proxy['http-opts']?.headers?.Host;
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['http-opts'].path}`, `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
'http-opts.path', 'http-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['http-opts'].headers.Host}`, `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
'http-opts.headers.Host', 'http-opts.headers.Host',
); );
} else { } else {
@@ -189,6 +194,9 @@ function vmess(proxy) {
} }
function vless(proxy) { function vless(proxy) {
if (proxy['reality-opts']) {
throw new Error(`reality is unsupported`);
}
const result = new Result(proxy); const result = new Result(proxy);
result.append( result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`, `${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
@@ -199,21 +207,23 @@ function vless(proxy) {
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
result.append(`,transport=ws`); result.append(`,transport=ws`);
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`, `,path=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`, `,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
result.append(`,transport=http`); result.append(`,transport=http`);
let httpPath = proxy['http-opts']?.path;
let httpHost = proxy['http-opts']?.headers?.Host;
result.appendIfPresent( result.appendIfPresent(
`,path=${proxy['http-opts'].path}`, `,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
'http-opts.path', 'http-opts.path',
); );
result.appendIfPresent( result.appendIfPresent(
`,host=${proxy['http-opts'].headers.Host}`, `,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
'http-opts.headers.Host', 'http-opts.headers.Host',
); );
} else { } else {
@@ -266,3 +276,94 @@ function http(proxy) {
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString(); return result.toString();
} }
function wireguard(proxy) {
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
proxy.server = proxy.peers[0].server;
proxy.port = proxy.peers[0].port;
proxy.ip = proxy.peers[0].ip;
proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-key'];
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
proxy.reserved = proxy.peers[0].reserved;
}
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
result.appendIfPresent(
`,private-key="${proxy['private-key']}"`,
'private-key',
);
result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
if (proxy.dns) {
if (Array.isArray(proxy.dns)) {
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
proxy.dns = proxy.dns.find((i) => isIPv4(i));
}
}
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
result.appendIfPresent(
`,keepalive=${proxy['persistent-keepalive']}`,
'persistent-keepalive',
);
result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
const allowedIps = Array.isArray(proxy['allowed-ips'])
? proxy['allowed-ips'].join(',')
: proxy['allowed-ips'];
let reserved = Array.isArray(proxy.reserved)
? proxy.reserved.join(',')
: proxy.reserved;
if (reserved) {
reserved = `,reserved=[${reserved}]`;
}
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
if (presharedKey) {
presharedKey = `,preshared-key="${presharedKey}"`;
}
result.append(
`,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
allowedIps ?? '0.0.0.0/0,::/0'
}",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
presharedKey ?? ''
}}]`,
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}

View File

@@ -62,6 +62,20 @@ function shadowsocks(proxy) {
); );
} }
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -74,6 +88,7 @@ function shadowsocks(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo // tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -81,6 +96,12 @@ function shadowsocks(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -113,6 +134,12 @@ function shadowsocksr(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -133,11 +160,11 @@ function trojan(proxy) {
if (needTls(proxy)) append(`,obfs=wss`); if (needTls(proxy)) append(`,obfs=wss`);
else append(`,obfs=ws`); else append(`,obfs=ws`);
appendIfPresent( appendIfPresent(
`,obfs-uri=${proxy['ws-opts'].path}`, `,obfs-uri=${proxy['ws-opts']?.path}`,
'ws-opts.path', 'ws-opts.path',
); );
appendIfPresent( appendIfPresent(
`,obfs-host=${proxy['ws-opts'].headers.Host}`, `,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host', 'ws-opts.headers.Host',
); );
} else { } else {
@@ -150,6 +177,20 @@ function trojan(proxy) {
append(`,over-tls=true`); append(`,over-tls=true`);
} }
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -162,6 +203,7 @@ function trojan(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo // tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -169,6 +211,12 @@ function trojan(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -206,12 +254,18 @@ function vmess(proxy) {
} else { } else {
throw new Error(`network ${proxy.network} is unsupported`); throw new Error(`network ${proxy.network} is unsupported`);
} }
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
appendIfPresent( appendIfPresent(
`,obfs-uri=${proxy[`${proxy.network}-opts`].path}`, `,obfs-uri=${
Array.isArray(transportPath) ? transportPath[0] : transportPath
}`,
`${proxy.network}-opts.path`, `${proxy.network}-opts.path`,
); );
appendIfPresent( appendIfPresent(
`,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`, `,obfs-host=${
Array.isArray(transportHost) ? transportHost[0] : transportHost
}`,
`${proxy.network}-opts.headers.Host`, `${proxy.network}-opts.headers.Host`,
); );
} else { } else {
@@ -219,6 +273,20 @@ function vmess(proxy) {
if (proxy.tls) append(`,obfs=over-tls`); if (proxy.tls) append(`,obfs=over-tls`);
} }
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -231,6 +299,7 @@ function vmess(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// AEAD // AEAD
if (isPresent(proxy, 'aead')) { if (isPresent(proxy, 'aead')) {
@@ -245,6 +314,12 @@ function vmess(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -266,6 +341,20 @@ function http(proxy) {
} }
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -278,6 +367,7 @@ function http(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo // tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -285,6 +375,12 @@ function http(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -306,6 +402,20 @@ function socks5(proxy) {
} }
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls'); appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint // tls fingerprint
appendIfPresent( appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`, `,tls-cert-sha256=${proxy['tls-fingerprint']}`,
@@ -318,6 +428,7 @@ function socks5(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni'); appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo // tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -325,6 +436,12 @@ function socks5(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag // tag
append(`,tag=${proxy.name}`); append(`,tag=${proxy.name}`);
@@ -332,11 +449,5 @@ function socks5(proxy) {
} }
function needTls(proxy) { function needTls(proxy) {
return ( return proxy.tls;
proxy.tls ||
proxy.sni ||
typeof proxy['skip-cert-verify'] !== 'undefined' ||
typeof proxy['tls-fingerprint'] !== 'undefined' ||
typeof proxy['tls-host'] !== 'undefined'
);
} }

View File

@@ -0,0 +1,163 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function ShadowRocket_Producer() {
const type = 'ALL';
const produce = (proxies) => {
return (
'proxies:\n' +
proxies
.filter((proxy) => {
if (
proxy.type === 'snell' &&
String(proxy.version) === '4'
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'hysteria2') {
if (
proxy['obfs-password'] &&
proxy.obfs == 'salamander'
) {
proxy.obfs = proxy['obfs-password'];
delete proxy['obfs-password'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
};
return { type, produce };
}

View File

@@ -3,9 +3,51 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Stash_Producer() { export default function Stash_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies) => { const produce = (proxies) => {
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
return ( return (
'proxies:\n' + 'proxies:\n' +
proxies proxies
.filter((proxy) => {
if (
![
'ss',
'ssr',
'vmess',
'socks5',
'http',
'snell',
'trojan',
'tuic',
'vless',
'wireguard',
'hysteria',
'hysteria2',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' &&
String(proxy.version) === '4') ||
(proxy.type === 'vless' && proxy['reality-opts'])
) {
return false;
}
return true;
})
.map((proxy) => { .map((proxy) => {
if (proxy.type === 'vmess') { if (proxy.type === 'vmess') {
// handle vmess aead // handle vmess aead
@@ -19,9 +61,178 @@ export default function Stash_Producer() {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
delete proxy.sni; delete proxy.sni;
} }
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L400
// https://stash.wiki/proxy-protocols/proxy-types#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'tuic') {
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
} else {
proxy.alpn = ['h3'];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
if (
(!proxy.token || proxy.token.length === 0) &&
!isPresent(proxy, 'version')
) {
proxy.version = 5;
}
} else if (proxy.type === 'hysteria') {
// auth_str 将会在未来某个时候删除 但是有的机场不规范
if (
isPresent(proxy, 'auth_str') &&
!isPresent(proxy, 'auth-str')
) {
proxy['auth-str'] = proxy['auth_str'];
}
if (isPresent(proxy, 'alpn')) {
proxy.alpn = Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn];
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
if (
isPresent(proxy, 'down') &&
!isPresent(proxy, 'down-speed')
) {
proxy['down-speed'] = proxy.down;
delete proxy.down;
}
if (
isPresent(proxy, 'up') &&
!isPresent(proxy, 'up-speed')
) {
proxy['up-speed'] = proxy.up;
delete proxy.up;
}
if (isPresent(proxy, 'down-speed')) {
proxy['down-speed'] =
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
}
if (isPresent(proxy, 'up-speed')) {
proxy['up-speed'] =
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
}
} else if (proxy.type === 'hysteria2') {
if (
isPresent(proxy, 'password') &&
!isPresent(proxy, 'auth')
) {
proxy.auth = proxy.password;
delete proxy.password;
}
if (
isPresent(proxy, 'tfo') &&
!isPresent(proxy, 'fast-open')
) {
proxy['fast-open'] = proxy.tfo;
delete proxy.tfo;
}
if (
isPresent(proxy, 'down') &&
!isPresent(proxy, 'down-speed')
) {
proxy['down-speed'] = proxy.down;
delete proxy.down;
}
if (
isPresent(proxy, 'up') &&
!isPresent(proxy, 'up-speed')
) {
proxy['up-speed'] = proxy.up;
delete proxy.up;
}
if (isPresent(proxy, 'down-speed')) {
proxy['down-speed'] =
`${proxy['down-speed']}`.match(/\d+/)?.[0] || 0;
}
if (isPresent(proxy, 'up-speed')) {
proxy['up-speed'] =
`${proxy['up-speed']}`.match(/\d+/)?.[0] || 0;
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} }
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['test-url']) {
proxy['benchmark-url'] = proxy['test-url'];
delete proxy['test-url'];
}
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return ' - ' + JSON.stringify(proxy) + '\n'; return ' - ' + JSON.stringify(proxy) + '\n';
}) })
.join('') .join('')

View File

@@ -0,0 +1,199 @@
import { Result, isPresent } from './utils';
import { isNotBlank } from '@/utils';
// import $ from '@/core/app';
const targetPlatform = 'Surfboard';
export default function Surfboard_Producer() {
const produce = (proxy) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy);
case 'http':
return http(proxy);
case 'socks5':
return socks5(proxy);
case 'wireguard-surge':
return wireguard(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
);
};
return { produce };
}
function shadowsocks(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
// obfs
if (isPresent(proxy, 'plugin')) {
if (proxy.plugin === 'obfs') {
result.append(`,obfs=${proxy['plugin-opts'].mode}`);
result.appendIfPresent(
`,obfs-host=${proxy['plugin-opts'].host}`,
'plugin-opts.host',
);
result.appendIfPresent(
`,obfs-uri=${proxy['plugin-opts'].path}`,
'plugin-opts.path',
);
} else {
throw new Error(`plugin ${proxy.plugin} is not supported`);
}
}
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function trojan(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
// transport
handleTransport(result, proxy);
// tls
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function vmess(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
// transport
handleTransport(result, proxy);
// AEAD
if (isPresent(proxy, 'aead')) {
result.append(`,vmess-aead=${proxy.aead}`);
} else {
result.append(`,vmess-aead=${proxy.alterId === 0}`);
}
// tls
result.appendIfPresent(`,tls=${proxy.tls}`, 'tls');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function http(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function socks5(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
return result.toString();
}
function wireguard(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
return result.toString();
}
function handleTransport(result, proxy) {
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,ws=true`);
if (isPresent(proxy, 'ws-opts')) {
result.appendIfPresent(
`,ws-path=${proxy['ws-opts'].path}`,
'ws-opts.path',
);
if (isPresent(proxy, 'ws-opts.headers')) {
const headers = proxy['ws-opts'].headers;
const value = Object.keys(headers)
.map((k) => {
let v = headers[k];
if (['Host'].includes(k)) {
v = `"${v}"`;
}
return `${k}:${v}`;
})
.join('|');
if (isNotBlank(value)) {
result.append(`,ws-headers=${value}`);
}
}
}
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
}
}

View File

@@ -4,6 +4,14 @@ import $ from '@/core/app';
const targetPlatform = 'Surge'; const targetPlatform = 'Surge';
const ipVersions = {
dual: 'dual',
ipv4: 'v4-only',
ipv6: 'v6-only',
'ipv4-prefer': 'prefer-v4',
'ipv6-prefer': 'prefer-v6',
};
export default function Surge_Producer() { export default function Surge_Producer() {
const produce = (proxy) => { const produce = (proxy) => {
switch (proxy.type) { switch (proxy.type) {
@@ -19,6 +27,12 @@ export default function Surge_Producer() {
return socks5(proxy); return socks5(proxy);
case 'snell': case 'snell':
return snell(proxy); return snell(proxy);
case 'tuic':
return tuic(proxy);
case 'wireguard-surge':
return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
} }
throw new Error( throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`, `Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -33,6 +47,16 @@ function shadowsocks(proxy) {
result.append(`,encrypt-method=${proxy.cipher}`); result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// obfs // obfs
if (isPresent(proxy, 'plugin')) { if (isPresent(proxy, 'plugin')) {
if (proxy.plugin === 'obfs') { if (proxy.plugin === 'obfs') {
@@ -55,6 +79,33 @@ function shadowsocks(proxy) {
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -63,6 +114,16 @@ function trojan(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// transport // transport
handleTransport(result, proxy); handleTransport(result, proxy);
@@ -87,6 +148,33 @@ function trojan(proxy) {
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -95,6 +183,16 @@ function vmess(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid'); result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// transport // transport
handleTransport(result, proxy); handleTransport(result, proxy);
@@ -127,6 +225,32 @@ function vmess(proxy) {
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -137,6 +261,16 @@ function http(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls fingerprint // tls fingerprint
result.appendIfPresent( result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
@@ -155,6 +289,33 @@ function http(proxy) {
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -165,6 +326,16 @@ function socks5(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls fingerprint // tls fingerprint
result.appendIfPresent( result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`, `,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
@@ -185,6 +356,33 @@ function socks5(proxy) {
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString(); return result.toString();
} }
@@ -194,22 +392,263 @@ function snell(proxy) {
result.appendIfPresent(`,version=${proxy.version}`, 'version'); result.appendIfPresent(`,version=${proxy.version}`, 'version');
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk'); result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// obfs // obfs
result.appendIfPresent( result.appendIfPresent(
`,obfs=${proxy['obfs-opts'].mode}`, `,obfs=${proxy['obfs-opts']?.mode}`,
'obfs-opts.mode', 'obfs-opts.mode',
); );
result.appendIfPresent( result.appendIfPresent(
`,obfs-host=${proxy['obfs-opts'].host}`, `,obfs-host=${proxy['obfs-opts']?.host}`,
'obfs-opts.host', 'obfs-opts.host',
); );
result.appendIfPresent( result.appendIfPresent(
`,obfs-uri=${proxy['obfs-opts'].path}`, `,obfs-uri=${proxy['obfs-opts']?.path}`,
'obfs-opts.path', 'obfs-opts.path',
); );
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// reuse
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
return result.toString();
}
function tuic(proxy) {
const result = new Result(proxy);
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
let type = proxy.type;
if (!proxy.token || proxy.token.length === 0) {
type = 'tuic-v5';
}
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
`,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
'alpn',
);
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tls fingerprint
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}
function wireguard(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString(); return result.toString();
} }
@@ -225,7 +664,13 @@ function handleTransport(result, proxy) {
if (isPresent(proxy, 'ws-opts.headers')) { if (isPresent(proxy, 'ws-opts.headers')) {
const headers = proxy['ws-opts'].headers; const headers = proxy['ws-opts'].headers;
const value = Object.keys(headers) const value = Object.keys(headers)
.map((k) => `${k}:${headers[k]}`) .map((k) => {
let v = headers[k];
if (['Host'].includes(k)) {
v = `"${v}"`;
}
return `${k}:${v}`;
})
.join('|'); .join('|');
if (isNotBlank(value)) { if (isNotBlank(value)) {
result.append(`,ws-headers=${value}`); result.append(`,ws-headers=${value}`);

View File

@@ -0,0 +1,50 @@
import { Result } from './utils';
import Surge_Producer from './surge';
// const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() {
const produce = (proxy) => {
switch (proxy.type) {
case 'ssr':
return shadowsocksr(proxy);
default:
return surge_Producer.produce(proxy);
}
};
return { produce };
}
function shadowsocksr(proxy) {
const result = new Result(proxy);
proxy.local_port = '__SubStoreLocalPort__';
proxy.local_address = proxy.local_address ?? '127.0.0.1';
result.append(
`${proxy.name} = external, exec = "${
proxy.exec || '/usr/local/bin/ssr-local'
}", address = "${proxy.server}", local-port = ${proxy.local_port}`,
);
for (const [key, value] of Object.entries({
cipher: '-m',
obfs: '-o',
password: '-k',
port: '-p',
protocol: '-O',
'protocol-param': '-G',
server: '-s',
local_port: '-l',
local_address: '-b',
})) {
result.appendIfPresent(
`, args = "${value}", args = "${proxy[key]}"`,
key,
);
}
return result.toString();
}

View File

@@ -1,10 +1,14 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { isIPv6 } from '@/utils';
export default function URI_Producer() { export default function URI_Producer() {
const type = 'SINGLE'; const type = 'SINGLE';
const produce = (proxy) => { const produce = (proxy) => {
let result = ''; let result = '';
if (proxy.server && isIPv6(proxy.server)) {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`; const userinfo = `${proxy.cipher}:${proxy.password}`;
@@ -55,27 +59,225 @@ export default function URI_Producer() {
break; break;
case 'vmess': case 'vmess':
// V2RayN URI format // V2RayN URI format
let type = '';
let net = proxy.network || 'tcp';
if (proxy.network === 'http') {
net = 'tcp';
type = 'http';
}
result = { result = {
v: '2',
ps: proxy.name, ps: proxy.name,
add: proxy.server, add: proxy.server,
port: proxy.port, port: proxy.port,
id: proxy.uuid, id: proxy.uuid,
type: '', type,
aid: 0, aid: 0,
net: proxy.network || 'tcp', net,
tls: proxy.tls ? 'tls' : '', tls: proxy.tls ? 'tls' : '',
}; };
if (proxy.tls && proxy.sni) {
result.sni = proxy.sni;
}
// obfs // obfs
if (proxy.network === 'ws') { if (proxy.network) {
result.path = proxy['ws-opts'].path || '/'; let vmessTransportPath =
result.host = proxy['ws-opts'].headers.Host || proxy.server; proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
'grpc-service-name'
];
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
}
} }
result = 'vmess://' + Base64.encode(JSON.stringify(result)); result = 'vmess://' + Base64.encode(JSON.stringify(result));
break; break;
case 'vless':
let security = 'none';
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
let alpn = '';
if (proxy.alpn) {
alpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
let allowInsecure = '';
if (proxy['skip-cert-verify']) {
allowInsecure = `&allowInsecure=1`;
}
let sni = '';
if (proxy.sni) {
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
}
let fp = '';
if (proxy['client-fingerprint']) {
fp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let flow = '';
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let vlessTransport = `&type=${encodeURIComponent(
proxy.network,
)}`;
if (['grpc'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
}
let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
let vlessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vlessTransportPath) {
vlessTransport += `&path=${encodeURIComponent(
Array.isArray(vlessTransportPath)
? vlessTransportPath[0]
: vlessTransportPath,
)}`;
}
if (vlessTransportHost) {
vlessTransport += `&host=${encodeURIComponent(
Array.isArray(vlessTransportHost)
? vlessTransportHost[0]
: vlessTransportHost,
)}`;
}
if (vlessTransportServiceName) {
vlessTransport += `&serviceName=${encodeURIComponent(
vlessTransportServiceName,
)}`;
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?${vlessTransport}&security=${encodeURIComponent(
security,
)}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'trojan': case 'trojan':
let trojanTransport = '';
if (proxy.network) {
trojanTransport = `&type=${proxy.network}`;
if (['grpc'].includes(proxy.network)) {
let trojanTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
)}`;
}
let trojanTransportPath =
proxy[`${proxy.network}-opts`]?.path;
let trojanTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (trojanTransportPath) {
trojanTransport += `&path=${encodeURIComponent(
Array.isArray(trojanTransportPath)
? trojanTransportPath[0]
: trojanTransportPath,
)}`;
}
if (trojanTransportHost) {
trojanTransport += `&host=${encodeURIComponent(
Array.isArray(trojanTransportHost)
? trojanTransportHost[0]
: trojanTransportHost,
)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${ result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port proxy.port
}#${encodeURIComponent(proxy.name)}`; }?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
break;
case 'hysteria2':
let hysteria2params = [];
if (proxy['skip-cert-verify']) {
hysteria2params.push(`insecure=1`);
}
if (proxy.obfs) {
hysteria2params.push(
`obfs=${encodeURIComponent(proxy.obfs)}`,
);
if (proxy['obfs-password']) {
hysteria2params.push(
`obfs-password=${encodeURIComponent(
proxy['obfs-password'],
)}`,
);
}
}
if (proxy.sni) {
hysteria2params.push(
`sni=${encodeURIComponent(proxy.sni)}`,
);
}
if (proxy['tls-fingerprint']) {
hysteria2params.push(
`pinSHA256=${encodeURIComponent(
proxy['tls-fingerprint'],
)}`,
);
}
if (proxy.tfo) {
hysteria2params.push(`fastopen=1`);
}
result = `hysteria2://${encodeURIComponent(proxy.password)}@${
proxy.server
}:${proxy.port}?${hysteria2params.join(
'&',
)}#${encodeURIComponent(proxy.name)}`;
break; break;
} }
return result; return result;

View File

@@ -0,0 +1,12 @@
/* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64';
import URI_Producer from './uri';
const URI = URI_Producer();
export default function V2Ray_Producer() {
const type = 'ALL';
const produce = (proxies) =>
Base64.encode(proxies.map((proxy) => URI.produce(proxy)).join('\n'));
return { type, produce };
}

View File

@@ -47,7 +47,7 @@ function AllRuleParser() {
} }
if (!matched) throw new Error('Invalid rule type: ' + rawType); if (!matched) throw new Error('Invalid rule type: ' + rawType);
} catch (e) { } catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`); console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
} }
} }
return result; return result;

View File

@@ -7,7 +7,7 @@
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash. * Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @author: Peng-YM * @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store * @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46 * @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/ */
import { version } from '../package.json'; import { version } from '../package.json';

View File

@@ -20,6 +20,8 @@ import registerArtifactRoutes from '@/restful/artifacts';
import registerSettingRoutes from '@/restful/settings'; import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs'; import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort'; import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerModuleRoutes from '@/restful/module';
migrate(); migrate();
serve(); serve();
@@ -30,6 +32,8 @@ function serve() {
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerSortRoutes($app); registerSortRoutes($app);

View File

@@ -19,7 +19,10 @@ export default function register($app) {
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY); if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
// RESTful APIs // RESTful APIs
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact); $app.route('/api/artifacts')
.get(getAllArtifacts)
.post(createArtifact)
.put(replaceArtifact);
$app.route('/api/artifact/:name') $app.route('/api/artifact/:name')
.get(getArtifact) .get(getArtifact)
@@ -32,6 +35,12 @@ function getAllArtifacts(req, res) {
success(res, allArtifacts); success(res, allArtifacts);
} }
function replaceArtifact(req, res) {
const allArtifacts = req.body;
$.write(allArtifacts, ARTIFACTS_KEY);
success(res);
}
async function getArtifact(req, res) { async function getArtifact(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
@@ -131,7 +140,12 @@ async function deleteArtifact(req, res) {
files[encodeURIComponent(artifact.name)] = { files[encodeURIComponent(artifact.name)] = {
content: '', content: '',
}; };
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
try {
await syncToGist(files); await syncToGist(files);
} catch (i) {
$.error(`Function syncToGist: ${name} : ${i}`);
}
} }
// delete local cache // delete local cache
deleteByName(allArtifacts, name); deleteByName(allArtifacts, name);

View File

@@ -14,13 +14,24 @@ export default function register($app) {
$app.route('/api/collections') $app.route('/api/collections')
.get(getAllCollections) .get(getAllCollections)
.post(createCollection); .post(createCollection)
.put(replaceCollection);
} }
// collection API // collection API
function createCollection(req, res) { function createCollection(req, res) {
const collection = req.body; const collection = req.body;
$.info(`正在创建组合订阅:${collection.name}`); $.info(`正在创建组合订阅:${collection.name}`);
if (/\//.test(collection.name)) {
failed(
res,
new RequestInvalidError(
'INVALID_NAME',
`Collection ${collection.name} is invalid`,
),
);
return;
}
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
if (findByName(allCols, collection.name)) { if (findByName(allCols, collection.name)) {
failed( failed(
@@ -30,6 +41,7 @@ function createCollection(req, res) {
`Collection ${collection.name} already exists.`, `Collection ${collection.name} already exists.`,
), ),
); );
return;
} }
allCols.push(collection); allCols.push(collection);
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
@@ -111,3 +123,9 @@ function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
success(res, allCols); success(res, allCols);
} }
function replaceCollection(req, res) {
const allCols = req.body;
$.write(allCols, COLLECTIONS_KEY);
success(res);
}

View File

@@ -20,6 +20,27 @@ async function downloadSubscription(req, res) {
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`); $.info(`正在下载订阅:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteSub } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
@@ -29,14 +50,27 @@ async function downloadSubscription(req, res) {
type: 'subscription', type: 'subscription',
name, name,
platform, platform,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
}); });
if (sub.source !== 'local') { if (sub.source !== 'local' || url) {
try {
// forward flow headers // forward flow headers
const flowInfo = await getFlowHeaders(sub.url); const flowInfo = await getFlowHeaders(url || sub.url);
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set('subscription-userinfo', flowInfo);
} }
} catch (err) {
$.error(
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
} }
if (platform === 'JSON') { if (platform === 'JSON') {
@@ -50,15 +84,15 @@ async function downloadSubscription(req, res) {
$.notify( $.notify(
`🌍 Sub-Store 下载订阅失败`, `🌍 Sub-Store 下载订阅失败`,
`❌ 无法下载订阅:${name}`, `❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`, `🤔 原因:${err.message ?? err}`,
); );
$.error(JSON.stringify(err)); $.error(err.message ?? err);
failed( failed(
res, res,
new InternalServerError( new InternalServerError(
'INTERNAL_SERVER_ERROR', 'INTERNAL_SERVER_ERROR',
`Failed to download subscription: ${name}`, `Failed to download subscription: ${name}`,
`Reason: ${JSON.stringify(err)}`, `Reason: ${err.message ?? err}`,
), ),
); );
} }
@@ -87,12 +121,20 @@ async function downloadCollection(req, res) {
$.info(`正在下载组合订阅:${name}`); $.info(`正在下载组合订阅:${name}`);
let { ignoreFailedRemoteSub } = req.query;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (collection) { if (collection) {
try { try {
const output = await produceArtifact({ const output = await produceArtifact({
type: 'collection', type: 'collection',
name, name,
platform, platform,
ignoreFailedRemoteSub,
}); });
// forward flow header from the first subscription in this collection // forward flow header from the first subscription in this collection
@@ -101,10 +143,18 @@ async function downloadCollection(req, res) {
if (subnames.length > 0) { if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]); const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') { if (sub.source !== 'local') {
try {
const flowInfo = await getFlowHeaders(sub.url); const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set('subscription-userinfo', flowInfo);
} }
} catch (err) {
$.error(
`组合订阅 ${name} 中的子订阅 ${
sub.name
} 获取流量信息时发生错误: ${err.message ?? err}`,
);
}
} }
} }
@@ -126,7 +176,7 @@ async function downloadCollection(req, res) {
new InternalServerError( new InternalServerError(
'INTERNAL_SERVER_ERROR', 'INTERNAL_SERVER_ERROR',
`Failed to download collection: ${name}`, `Failed to download collection: ${name}`,
`Reason: ${JSON.stringify(err)}`, `Reason: ${err.message ?? err}`,
), ),
); );
} }

109
backend/src/restful/file.js Normal file
View File

@@ -0,0 +1,109 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
}
// file API
function createFile(req, res) {
const file = req.body;
file.name = `${file.name ?? Date.now()}`;
$.info(`正在创建文件:${file.name}`);
const allFiles = $.read(FILES_KEY);
if (findByName(allFiles, file.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${file.name} 的文件`
: `无法同时创建相同的文件 可稍后重试`,
),
);
}
allFiles.push(file);
$.write(allFiles, FILES_KEY);
success(res, file, 201);
}
function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
res.status(200).json(file.content);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function updateFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let file = req.body;
const allFiles = $.read(FILES_KEY);
const oldFile = findByName(allFiles, name);
if (oldFile) {
const newFile = {
...oldFile,
...file,
};
$.info(`正在更新文件:${name}...`);
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function deleteFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除文件:${name}`);
let allFiles = $.read(FILES_KEY);
deleteByName(allFiles, name);
$.write(allFiles, FILES_KEY);
success(res);
}
function getAllFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(
res, // eslint-disable-next-line no-unused-vars
allFiles.map(({ content, ...rest }) => rest),
);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);
success(res);
}

View File

@@ -1,9 +1,13 @@
import express from '@/vendor/express'; import express from '@/vendor/express';
import $ from '@/core/app'; import $ from '@/core/app';
import migrate from '@/utils/migration';
import download from '@/utils/download';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts'; import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync'; import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download'; import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings'; import registerSettingRoutes from './settings';
@@ -13,8 +17,13 @@ import registerMiscRoutes from './miscs';
import registerNodeInfoRoutes from './node-info'; import registerNodeInfoRoutes from './node-info';
export default function serve() { export default function serve() {
const $app = express({ substore: $ }); let port;
let host;
if ($.env.isNode) {
port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
}
const $app = express({ substore: $, port, host });
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
@@ -23,9 +32,127 @@ export default function serve() {
registerSortingRoutes($app); registerSortingRoutes($app);
registerSettingRoutes($app); registerSettingRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app); registerSyncRoutes($app);
registerNodeInfoRoutes($app); registerNodeInfoRoutes($app);
registerMiscRoutes($app); registerMiscRoutes($app);
$app.start(); $app.start();
if ($.env.isNode) {
const path = eval(`require("path")`);
const fs = eval(`require("fs")`);
const data_url = eval('process.env.SUB_STORE_DATA_URL');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
const fe_host =
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
const fe_abs_path = path.resolve(
fe_path || path.join(__dirname, 'frontend'),
);
if (fe_path) {
try {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {
throw new Error(
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
);
}
const express_ = eval(`require("express")`);
const history = eval(`require("connect-history-api-fallback")`);
const { createProxyMiddleware } = eval(
`require("http-proxy-middleware")`,
);
const app = express_();
const staticFileMiddleware = express_.static(fe_path);
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/';
let be_download = '/download/';
if (fe_be_path) {
if (!fe_be_path.startsWith('/')) {
throw new Error(
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
);
}
be_api_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_api}`;
be_download_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_download}`;
app.use(
be_api_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_api_rewrite)
? path.replace(be_api_rewrite, be_api)
: path;
},
}),
);
app.use(
be_download_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_download_rewrite)
? path.replace(be_download_rewrite, be_download)
: path;
},
}),
);
}
app.use(staticFileMiddleware);
app.use(
history({
disableDotRule: true,
verbose: false,
}),
);
app.use(staticFileMiddleware);
const listener = app.listen(fe_port, fe_host, () => {
const { address: fe_address, port: fe_port } =
listener.address();
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
if (fe_be_path) {
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
);
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
);
}
});
}
if (data_url) {
$.info(`[BACKEND] downloading data from ${data_url}`);
download(data_url)
.then((content) => {
$.write(content, '#sub-store');
$.cache = JSON.parse(content);
$.persistCache();
migrate();
$.info(`[BACKEND] restored data from ${data_url}`);
})
.catch((e) => {
$.error(`[BACKEND] restore data failed`);
console.error(e);
throw e;
});
}
}
} }

View File

@@ -1,7 +1,6 @@
import $ from '@/core/app'; import $ from '@/core/app';
import { ENV } from '@/vendor/open-api'; import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import { version as substoreVersion } from '../../package.json';
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings'; import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import { import {
@@ -12,6 +11,7 @@ import {
import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import env from '@/utils/env';
export default function register($app) { export default function register($app) {
// utils // utils
@@ -22,12 +22,26 @@ export default function register($app) {
// Storage management // Storage management
$app.route('/api/storage') $app.route('/api/storage')
.get((req, res) => { .get((req, res) => {
res.json($.read('#sub-store')); res.set('content-type', 'application/json')
.set(
'content-disposition',
'attachment; filename="sub-store.json"',
)
.send(
$.env.isNode
? JSON.stringify($.cache)
: $.read('#sub-store'),
);
}) })
.post((req, res) => { .post((req, res) => {
const data = req.body; const { content } = req.body;
$.write(JSON.stringify(data), '#sub-store'); $.write(content, '#sub-store');
res.end(); if ($.env.isNode) {
$.cache = JSON.parse(content);
$.persistCache();
}
migrate();
success(res);
}); });
// Redirect sub.store to vercel webpage // Redirect sub.store to vercel webpage
@@ -49,19 +63,7 @@ export default function register($app) {
} }
function getEnv(req, res) { function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV(); success(res, env);
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
} }
async function refresh(_, res) { async function refresh(_, res) {
@@ -118,6 +120,23 @@ async function gistBackup(req, res) {
case 'download': case 'download':
$.info(`还原备份中...`); $.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME); 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 // restore settings
$.write(content, '#sub-store'); $.write(content, '#sub-store');
if ($.env.isNode) { if ($.env.isNode) {
@@ -125,8 +144,10 @@ async function gistBackup(req, res) {
$.cache = content; $.cache = content;
$.persistCache(); $.persistCache();
} }
// perform migration after restoring from gist $.info(`perform migration after restoring from gist...`);
migrate(); migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break; break;
} }
success(res); success(res);
@@ -136,7 +157,7 @@ async function gistBackup(req, res) {
new InternalServerError( new InternalServerError(
'BACKUP_FAILED', 'BACKUP_FAILED',
`Failed to ${action} data to gist!`, `Failed to ${action} data to gist!`,
`Reason: ${JSON.stringify(err)}`, `Reason: ${err.message ?? err}`,
), ),
); );
} }

View File

@@ -0,0 +1,114 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { MODULES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { hex_md5 } from '@/vendor/md5';
export default function register($app) {
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
$app.route('/api/module/:name')
.get(getModule)
.patch(updateModule)
.delete(deleteModule);
$app.route('/api/modules')
.get(getAllModules)
.post(createModule)
.put(replaceModule);
}
// module API
function createModule(req, res) {
const module = req.body;
module.name = `${module.name ?? hex_md5(JSON.stringify(module))}`;
$.info(`正在创建模块:${module.name}`);
const allModules = $.read(MODULES_KEY);
if (findByName(allModules, module.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${module.name} 的模块`
: `已存在相同的模块 请勿重复添加`,
),
);
}
allModules.push(module);
$.write(allModules, MODULES_KEY);
success(res, module, 201);
}
function getModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
res.status(200).json(module.content);
} else {
failed(
res,
new ResourceNotFoundError(
`MODULE_NOT_FOUND`,
`Module ${name} does not exist`,
404,
),
);
}
}
function updateModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let module = req.body;
const allModules = $.read(MODULES_KEY);
const oldModule = findByName(allModules, name);
if (oldModule) {
const newModule = {
...oldModule,
...module,
};
$.info(`正在更新模块:${name}...`);
updateByName(allModules, name, newModule);
$.write(allModules, MODULES_KEY);
success(res, newModule);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Module ${name} does not exist!`,
),
404,
);
}
}
function deleteModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除模块:${name}`);
let allModules = $.read(MODULES_KEY);
deleteByName(allModules, name);
$.write(allModules, MODULES_KEY);
success(res);
}
function getAllModules(req, res) {
const allModules = $.read(MODULES_KEY);
success(
res,
// eslint-disable-next-line no-unused-vars
allModules.map(({ content, ...rest }) => rest),
);
}
function replaceModule(req, res) {
const allModules = req.body;
$.write(allModules, MODULES_KEY);
success(res);
}

View File

@@ -22,7 +22,10 @@ async function getNodeInfo(req, res) {
const info = await $http const info = await $http
.get({ .get({
url: `http://ip-api.com/json/${encodeURIComponent( url: `http://ip-api.com/json/${encodeURIComponent(
proxy.server, proxy.server
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '')
)}?lang=${lang}`, )}?lang=${lang}`,
headers: { headers: {
'User-Agent': 'User-Agent':

View File

@@ -1,4 +1,4 @@
import { InternalServerError, NetworkError } from './errors'; import { InternalServerError } from './errors';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import { findByName } from '@/utils/database'; import { findByName } from '@/utils/database';
import { success, failed } from './response'; import { success, failed } from './response';
@@ -12,32 +12,57 @@ export default function register($app) {
} }
async function compareSub(req, res) { async function compareSub(req, res) {
try {
const sub = req.body; const sub = req.body;
const target = req.query.target || 'JSON'; const target = req.query.target || 'JSON';
let content; let content;
if (sub.source === 'local') { if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
content = sub.content; content = sub.content;
} else { } else {
const errors = {};
content = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try { try {
content = await download(sub.url, sub.ua); return await download(url, sub.ua);
} catch (err) { } catch (err) {
failed( errors[url] = err;
res, $.error(
new NetworkError( `订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
); );
return; return '';
}
}),
);
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
content.push(sub.content);
} }
} }
// parse proxies // parse proxies
const original = ProxyUtils.parse(content); const original = (Array.isArray(content) ? content : [content])
.map((i) => ProxyUtils.parse(i))
.flat();
// add id // add id
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
proxy.id = i; proxy.id = i;
proxy.subName = sub.name;
}); });
// apply processors // apply processors
@@ -45,50 +70,119 @@ async function compareSub(req, res) {
original, original,
sub.process || [], sub.process || [],
target, target,
{ [sub.name]: sub },
); );
// produce // produce
success(res, { original, processed }); success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview subscription`,
`Reason: ${err.message ?? err}`,
),
);
}
} }
async function compareCollection(req, res) { async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const collection = req.body; const collection = req.body;
const subnames = collection.subscriptions; const subnames = collection.subscriptions;
const results = {}; const results = {};
const errors = {};
await Promise.all( await Promise.all(
subnames.map(async (name) => { subnames.map(async (name) => {
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
try { try {
let raw; let raw;
if (sub.source === 'local') { if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(
sub.mergeSources,
)
) {
raw = sub.content; raw = sub.content;
} else { } else {
raw = await download(sub.url, sub.ua); const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
} }
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors // apply processors
currentProxies = await ProxyUtils.process( currentProxies = await ProxyUtils.process(
currentProxies, currentProxies,
sub.process || [], sub.process || [],
'JSON', 'JSON',
{ [sub.name]: sub, _collection: collection },
); );
results[name] = currentProxies; results[name] = currentProxies;
} catch (err) { } catch (err) {
failed( errors[name] = err;
res,
new InternalServerError( $.error(
'PROCESS_FAILED', `❌ 处理组合订阅中的子订阅: ${
`处理子订阅 ${name} 失败`, sub.name
`Reason: ${err}`, }时出现错误:${err}!进度--${
), 100 * (processed / subnames.length).toFixed(1)
}%`,
); );
} }
}), }),
); );
if (
!collection.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${collection.name} 中的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
// merge proxies with the original order // merge proxies with the original order
const original = Array.prototype.concat.apply( const original = Array.prototype.concat.apply(
[], [],
@@ -97,13 +191,26 @@ async function compareCollection(req, res) {
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
proxy.id = i; proxy.id = i;
proxy.collectionName = collection.name;
}); });
const processed = await ProxyUtils.process( const processed = await ProxyUtils.process(
original, original,
collection.process || [], collection.process || [],
'JSON', 'JSON',
{ _collection: collection },
); );
success(res, { original, processed }); success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview collection`,
`Reason: ${err.message ?? err}`,
),
);
}
} }

View File

@@ -10,7 +10,12 @@ export default function register($app) {
} }
async function getSettings(req, res) { async function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY); let settings = $.read(SETTINGS_KEY);
if (!settings) {
settings = {};
$.write(settings, SETTINGS_KEY);
}
if (!settings.avatarUrl) await updateGitHubAvatar(); if (!settings.avatarUrl) await updateGitHubAvatar();
if (!settings.artifactStore) await updateArtifactStore(); if (!settings.artifactStore) await updateArtifactStore();
success(res, settings); success(res, settings);
@@ -44,8 +49,12 @@ export async function updateGitHubAvatar() {
.then((resp) => JSON.parse(resp.body)); .then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url']; settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} catch (e) { } catch (err) {
$.error('Failed to fetch GitHub avatar for User: ' + username); $.error(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
} }
} }
} }
@@ -67,7 +76,11 @@ export async function updateArtifactStore() {
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);
} }
} catch (err) { } catch (err) {
$.error('Failed to fetch artifact store for User: ' + githubUser); $.error(
`Failed to fetch artifact store for User: ${githubUser}. Reason: ${
err.message ?? err
}`,
);
} }
} }
} }

View File

@@ -11,7 +11,7 @@ export default function register($app) {
function sortSubs(req, res) { function sortSubs(req, res) {
const orders = req.body; const orders = req.body;
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
success(res, allSubs); success(res, allSubs);
} }
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
function sortCollections(req, res) { function sortCollections(req, res) {
const orders = req.body; const orders = req.body;
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
success(res, allCols); success(res, allCols);
} }
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
function sortArtifacts(req, res) { function sortArtifacts(req, res) {
const orders = req.body; const orders = req.body;
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b)); allArtifacts.sort(
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts); success(res, allArtifacts);
} }

View File

@@ -6,7 +6,7 @@ import {
} from './errors'; } from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
import { success, failed } from './response'; import { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -20,7 +20,10 @@ export default function register($app) {
.patch(updateSubscription) .patch(updateSubscription)
.delete(deleteSubscription); .delete(deleteSubscription);
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription); $app.route('/api/subs')
.get(getAllSubscriptions)
.post(createSubscription)
.put(replaceSubscriptions);
} }
// subscriptions API // subscriptions API
@@ -65,20 +68,7 @@ async function getFlowInfo(req, res) {
return; return;
} }
// unit is KB success(res, parseFlowHeaders(flowHeaders));
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/)
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/)
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/);
const expires = match ? Number(match[1]) : undefined;
success(res, { expires, total, usage: { upload, download } });
} catch (err) { } catch (err) {
failed( failed(
res, res,
@@ -93,6 +83,16 @@ async function getFlowInfo(req, res) {
function createSubscription(req, res) { function createSubscription(req, res) {
const sub = req.body; const sub = req.body;
$.info(`正在创建订阅: ${sub.name}`); $.info(`正在创建订阅: ${sub.name}`);
if (/\//.test(sub.name)) {
failed(
res,
new RequestInvalidError(
'INVALID_NAME',
`Subscription ${sub.name} is invalid`,
),
);
return;
}
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
if (findByName(allSubs, sub.name)) { if (findByName(allSubs, sub.name)) {
failed( failed(
@@ -102,6 +102,7 @@ function createSubscription(req, res) {
`Subscription ${sub.name} already exists.`, `Subscription ${sub.name} already exists.`,
), ),
); );
return;
} }
allSubs.push(sub); allSubs.push(sub);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);
@@ -202,3 +203,9 @@ function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
success(res, allSubs); success(res, allSubs);
} }
function replaceSubscriptions(req, res) {
const allSubs = req.body;
$.write(allSubs, SUBS_KEY);
success(res);
}

View File

@@ -22,36 +22,125 @@ export default function register($app) {
$app.get('/api/sync/artifact/:name', syncArtifact); $app.get('/api/sync/artifact/:name', syncArtifact);
} }
async function produceArtifact({ type, name, platform }) { async function produceArtifact({
type,
name,
platform,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
}) {
platform = platform || 'JSON'; platform = platform || 'JSON';
// produce Clash node format for ShadowRocket
if (platform === 'ShadowRocket') platform = 'Clash';
if (type === 'subscription') { if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
let raw; let raw;
if (sub.source === 'local') { if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
raw = sub.content; raw = sub.content;
} else { } else {
raw = await download(sub.url, sub.ua); const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
} }
// parse proxies // parse proxies
let proxies = ProxyUtils.parse(raw); let proxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
proxies.forEach((proxy) => {
proxy.subName = sub.name;
});
// apply processors // apply processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub },
); );
if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`);
}
// check duplicate // check duplicate
const exist = {}; const exist = {};
for (const proxy of proxies) { for (const proxy of proxies) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':
@@ -70,6 +159,7 @@ async function produceArtifact({ type, name, platform }) {
const collection = findByName(allCols, name); const collection = findByName(allCols, name);
const subnames = collection.subscriptions; const subnames = collection.subscriptions;
const results = {}; const results = {};
const errors = {};
let processed = 0; let processed = 0;
await Promise.all( await Promise.all(
@@ -78,18 +168,64 @@ async function produceArtifact({ type, name, platform }) {
try { try {
$.info(`正在处理子订阅:${sub.name}...`); $.info(`正在处理子订阅:${sub.name}...`);
let raw; let raw;
if (sub.source === 'local') { if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(
sub.mergeSources,
)
) {
raw = sub.content; raw = sub.content;
} else { } else {
raw = await download(sub.url, sub.ua); const errors = {};
raw = await await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
} }
// parse proxies // parse proxies
let currentProxies = ProxyUtils.parse(raw); let currentProxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors // apply processors
currentProxies = await ProxyUtils.process( currentProxies = await ProxyUtils.process(
currentProxies, currentProxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub, _collection: collection },
); );
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
@@ -100,31 +236,51 @@ async function produceArtifact({ type, name, platform }) {
); );
} catch (err) { } catch (err) {
processed++; processed++;
errors[name] = err;
$.error( $.error(
`❌ 处理组合订阅中的子订阅: ${ `❌ 处理组合订阅中的子订阅: ${
sub.name sub.name
}时出现错误:${err},该订阅已被跳过!进度--${ }时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1) 100 * (processed / subnames.length).toFixed(1)
}%`, }%`,
); );
} }
}), }),
); );
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (
!collectionIgnoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
// merge proxies with the original order // merge proxies with the original order
let proxies = Array.prototype.concat.apply( let proxies = Array.prototype.concat.apply(
[], [],
subnames.map((name) => results[name]), subnames.map((name) => results[name] || []),
); );
proxies.forEach((proxy) => {
proxy.collectionName = collection.name;
});
// apply own processors // apply own processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
proxies, proxies,
collection.process || [], collection.process || [],
platform, platform,
{ _collection: collection },
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点`); throw new Error(`组合订阅 ${name} 中不含有效节点`);
} }
// check duplicate // check duplicate
const exist = {}; const exist = {};
@@ -132,7 +288,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) { if (exist[proxy.name]) {
$.notify( $.notify(
'🌍 Sub-Store', '🌍 Sub-Store',
'⚠️ 订阅包含重复节点!', `⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!', '请仔细检测配置!',
{ {
'media-url': 'media-url':
@@ -230,10 +386,12 @@ async function syncAllArtifacts(_, res) {
async function syncArtifact(req, res) { async function syncArtifact(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
$.info(`开始同步远程配置 ${name}...`);
const allArtifacts = $.read(ARTIFACTS_KEY); const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name); const artifact = findByName(allArtifacts, name);
if (!artifact) { if (!artifact) {
$.error(`找不到远程配置 ${name}`);
failed( failed(
res, res,
new ResourceNotFoundError( new ResourceNotFoundError(
@@ -245,6 +403,7 @@ async function syncArtifact(req, res) {
return; return;
} }
try {
const output = await produceArtifact({ const output = await produceArtifact({
type: artifact.type, type: artifact.type,
name: artifact.source, name: artifact.source,
@@ -258,7 +417,6 @@ async function syncArtifact(req, res) {
2, 2,
)}`, )}`,
); );
try {
const resp = await syncToGist({ const resp = await syncToGist({
[encodeURIComponent(artifact.name)]: { [encodeURIComponent(artifact.name)]: {
content: output, content: output,
@@ -272,6 +430,7 @@ async function syncArtifact(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact); success(res, artifact);
} catch (err) { } catch (err) {
$.error(`远程配置 ${artifact.name} 发生错误: ${err}`);
failed( failed(
res, res,
new InternalServerError( new InternalServerError(

View File

@@ -1,28 +1,74 @@
import { HTTP } from '@/vendor/open-api'; import { FILES_KEY, MODULES_KEY, SETTINGS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download(url, ua) { export default async function download(url, ua, timeout) {
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)'; let $arguments = {};
const id = hex_md5(ua + url); const rawArgs = url.split('#');
if (tasks.has(id)) { if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到本地 ${type}: ${name}`);
}
return item.content;
}
const { isNode } = ENV();
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
if (!isNode && tasks.has(id)) {
return tasks.get(id); return tasks.get(id);
} }
const http = HTTP({ const http = HTTP({
headers: { headers: {
'User-Agent': ua, 'User-Agent': userAgent,
}, },
timeout: requestTimeout,
}); });
const result = new Promise((resolve, reject) => { const result = new Promise((resolve, reject) => {
// try to find in app cache // try to find in app cache
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (cached) { if (!$arguments?.noCache && cached) {
resolve(cached); resolve(cached);
} else { } else {
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
);
http.get(url) http.get(url)
.then((resp) => { .then((resp) => {
const body = resp.body; const body = resp.body;
@@ -39,6 +85,8 @@ export default async function download(url, ua) {
} }
}); });
if (!isNode) {
tasks.set(id, result); tasks.set(id, result);
}
return result; return result;
} }

16
backend/src/utils/env.js Normal file
View File

@@ -0,0 +1,16 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
export default {
backend,
version: substoreVersion,
};

View File

@@ -1,15 +1,52 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api'; import { HTTP } from '@/vendor/open-api';
import $ from '@/core/app';
export async function getFlowHeaders(url) { export async function getFlowHeaders(url, ua, timeout) {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP(); const http = HTTP();
const { headers } = await http.get({ const { headers } = await http.get({
url, url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: { headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)', 'User-Agent': userAgent,
}, },
timeout: requestTimeout,
}); });
const subkey = Object.keys(headers).filter((k) => const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k), /SUBSCRIPTION-USERINFO/i.test(k),
)[0]; )[0];
return headers[subkey]; return headers[subkey];
} }
export function parseFlowHeaders(flowHeaders) {
if (!flowHeaders) return;
// unit is KB
const uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/);
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/);
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/);
const expires = match ? Number(match[1]) : undefined;
return { expires, total, usage: { upload, download } };
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024
? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]);
}

View File

@@ -155,7 +155,7 @@ export function getFlag(name) {
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'], '🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
'🇲🇹': ['Malta', '马耳他'], '🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'], '🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'], '🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'], '🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇴': ['Norway', '挪威'], '🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'], '🇳🇵': ['Nepal', '尼泊尔'],

View File

@@ -40,6 +40,10 @@ export default class Gist {
} }
async upload(files) { async upload(files) {
if (Object.keys(files).length === 0) {
return Promise.reject('未提供需上传的文件');
}
const id = await this.locate(); const id = await this.locate();
if (id === -1) { if (id === -1) {

View File

@@ -13,6 +13,12 @@ function isIPv6(ip) {
return IPV6_REGEX.test(ip); return IPV6_REGEX.test(ip);
} }
function isValidPortNumber(port) {
return /^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$/.test(
port,
);
}
function isNotBlank(str) { function isNotBlank(str) {
return typeof str === 'string' && str.trim().length > 0; return typeof str === 'string' && str.trim().length > 0;
} }
@@ -29,4 +35,12 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue; return isPresent(obj) ? obj : defaultValue;
} }
export { isIPv4, isIPv6, isNotBlank, getIfNotBlank, isPresent, getIfPresent }; export {
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
getIfNotBlank,
isPresent,
getIfPresent,
};

View File

@@ -1,14 +1,20 @@
export function getPlatformFromHeaders(headers) { export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers); const keys = Object.keys(headers);
let UA = ''; let UA = '';
let ua = '';
for (let k of keys) { for (let k of keys) {
if (/USER-AGENT/i.test(k)) { if (/USER-AGENT/i.test(k)) {
UA = headers[k]; UA = headers[k];
ua = UA.toLowerCase();
break; break;
} }
} }
if (UA.indexOf('Quantumult%20X') !== -1) { if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX'; return 'QX';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) { } else if (UA.indexOf('Surge') !== -1) {
return 'Surge'; return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) { } else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
@@ -17,6 +23,15 @@ export function getPlatformFromHeaders(headers) {
return 'ShadowRocket'; return 'ShadowRocket';
} else if (UA.indexOf('Stash') !== -1) { } else if (UA.indexOf('Stash') !== -1) {
return 'Stash'; return 'Stash';
} else if (
ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1)
) {
return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else { } else {
return 'JSON'; return 'JSON';
} }

View File

@@ -48,7 +48,7 @@ class ResourceCache {
} }
set(id, value) { set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value } this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist(); this._persist();
} }
} }

View File

@@ -0,0 +1,105 @@
import $ from '@/core/app';
import {
SCRIPT_RESOURCE_CACHE_KEY,
CSR_EXPIRATION_TIME_KEY,
} from '@/constants';
class ResourceCache {
constructor() {
this.expires = getExpiredTime();
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(SCRIPT_RESOURCE_CACHE_KEY));
this._cleanup();
}
_cleanup() {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
});
if (clear) this._persist();
}
revokeAll() {
this.resourceCache = {};
this._persist();
}
_persist() {
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
}
get(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].data;
}
return null;
}
gettime(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].time;
}
return null;
}
set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
}
}
function getExpiredTime() {
// console.log($.read(CSR_EXPIRATION_TIME_KEY));
if (!$.read(CSR_EXPIRATION_TIME_KEY)) {
$.write('1728e5', CSR_EXPIRATION_TIME_KEY); // 48 * 3600 * 1000
}
let expiration = 1728e5;
if ($.env.isLoon) {
const loont = {
// Loon 插件自义定
'1\u5206\u949f': 6e4,
'5\u5206\u949f': 3e5,
'10\u5206\u949f': 6e5,
'30\u5206\u949f': 18e5, // "30分钟"
'1\u5c0f\u65f6': 36e5,
'2\u5c0f\u65f6': 72e5,
'3\u5c0f\u65f6': 108e5,
'6\u5c0f\u65f6': 216e5,
'12\u5c0f\u65f6': 432e5,
'24\u5c0f\u65f6': 864e5,
'48\u5c0f\u65f6': 1728e5,
'72\u5c0f\u65f6': 2592e5, // "72小时"
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
};
let intimed = $.read('#\u8282\u70b9\u7f13\u5b58\u6709\u6548\u671f'); // Loon #节点缓存有效期
// console.log(intimed);
if (intimed in loont) {
expiration = loont[intimed];
if (expiration === 'readcachets') {
expiration = intimed;
}
}
return expiration;
} else {
expiration = $.read(CSR_EXPIRATION_TIME_KEY);
return expiration;
}
}
export default new ResourceCache();

View File

@@ -1,8 +1,7 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import { ENV } from './open-api'; import { ENV } from './open-api';
export default function express({ substore: $, port }) { export default function express({ substore: $, port, host }) {
port = port || 3000;
const { isNode } = ENV(); const { isNode } = ENV();
const DEFAULT_HEADERS = { const DEFAULT_HEADERS = {
'Content-Type': 'text/plain;charset=UTF-8', 'Content-Type': 'text/plain;charset=UTF-8',
@@ -17,7 +16,7 @@ export default function express({ substore: $, port }) {
const express_ = eval(`require("express")`); const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`); const bodyParser = eval(`require("body-parser")`);
const app = express_(); const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver })); app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
app.use( app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }), bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
); );
@@ -29,8 +28,9 @@ export default function express({ substore: $, port }) {
// adapter // adapter
app.start = () => { app.start = () => {
app.listen(port, () => { const listener = app.listen(port, host, () => {
$.info(`Express started on port: ${port}`); const { address, port } = listener.address();
$.info(`[BACKEND] ${address}:${port}`);
}); });
}; };
return app; return app;

View File

@@ -49,27 +49,32 @@ export class OpenAPI {
if (isNode) { if (isNode) {
// create a json for root cache // create a json for root cache
let fpath = 'root.json'; const basePath =
if (!this.node.fs.existsSync(fpath)) { eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { let rootPath = `${basePath}/root.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.root = {}; this.root = {};
} else { } else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`)); this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
} }
// create a json file with the given name if not exists // create a json file with the given name if not exists
fpath = `${this.name}.json`; let fpath = `${basePath}/${this.name}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) { if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx', flag: 'wx',
}); });
this.cache = {}; this.cache = {};
} else { } else {
this.cache = JSON.parse( this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
this.node.fs.readFileSync(`${this.name}.json`),
);
} }
} }
} }
@@ -80,14 +85,17 @@ export class OpenAPI {
if (isQX) $prefs.setValueForKey(data, this.name); if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name); if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) { if (isNode) {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
`${this.name}.json`, `${basePath}/${this.name}.json`,
data, data,
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
); );
this.node.fs.writeFileSync( this.node.fs.writeFileSync(
'root.json', `${basePath}/root.json`,
JSON.stringify(this.root, null, 2), JSON.stringify(this.root, null, 2),
{ flag: 'w' }, { flag: 'w' },
(err) => console.log(err), (err) => console.log(err),
@@ -183,6 +191,32 @@ export class OpenAPI {
(openURL ? `\n点击跳转: ${openURL}` : '') + (openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : ''); (mediaURL ? `\n多媒体: ${mediaURL}` : '');
console.log(`${title}\n${subtitle}\n${content_}\n\n`); console.log(`${title}\n${subtitle}\n${content_}\n\n`);
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
if (push) {
const url = push
.replace(
'[推送标题]',
encodeURIComponent(title || 'Sub-Store'),
)
.replace(
'[推送内容]',
encodeURIComponent(
[subtitle, content_].map((i) => i).join('\n'),
),
);
const $http = HTTP();
$http
.get({ url })
.then((resp) => {
console.log(
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
);
})
.catch((e) => {
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
});
}
} }
} }
@@ -308,7 +342,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
return ( return (
timer timer
? Promise.race([timer, worker]).then((res) => { ? Promise.race([timer, worker]).then((res) => {
if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutid); clearTimeout(timeoutid);
}
return res; return res;
}) })
: worker : worker

File diff suppressed because one or more lines are too long

View File

@@ -2,14 +2,18 @@
#!desc=高级订阅管理工具 #!desc=高级订阅管理工具
#!openUrl=https://sub.store #!openUrl=https://sub.store
#!author=Peng-YM #!author=Peng-YM
#!homepage=https://github.com/Peng-YM/Sub-Store #!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png #!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
[Rule]
DOMAIN,sub-store.vercel.app,PROXY
[MITM] [MITM]
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://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync

7
config/QX-Task.json Normal file
View File

@@ -0,0 +1,7 @@
{
"name":"Sub-Store",
"description":"",
"task":[
"0 0 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
]
}

View File

@@ -1,20 +1,34 @@
# Sub-Store 配置指南 # Sub-Store 配置指南
## 查看更新说明:
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置: ## 脚本配置:
### 1. Loon ### 1. Loon
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。 安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge ### 2. Surge
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。 1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX ### 3. QX
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可 订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可
定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
### 4. Stash ### 4. Stash
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。 安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket ### 5. Shadowrocket
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。 安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
## 使用 Sub-Store ## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。 1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。

View File

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

View File

@@ -0,0 +1,11 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用不带 ability 参数版本
[MITM]
hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -1,10 +1,11 @@
#!name=Sub-Store #!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM #!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 可以用带 ability 参数
[MITM] [MITM]
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

40
package-lock.json generated
View File

@@ -1,40 +0,0 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
}