Compare commits

...

171 Commits

Author SHA1 Message Date
xream
cacc106c68
doc: README 2025-06-03 16:42:01 +08:00
xream
542fcc44a1
feat: 订阅和文件的远程链接支持使用换行混写三种格式 1. 完整远程链接 2. 类似 /api/file/name 的内部文件调用路径 3. 本地文件的绝对路径 2025-06-03 00:10:45 +08:00
xream
dca3d2f79c
fix: 脚本链接为路径时带参解析 2025-06-02 23:17:47 +08:00
xream
3e14f91347
feat: sing-box VLESS packet_encoding 2025-06-02 20:39:15 +08:00
xream
4aafdaaddb
feat: 支持本地文件 2025-06-01 11:54:32 +08:00
xream
e4f646af0c
feat: 若设置 $options._res.headers, 拉取文件时将设置自定义响应头 2025-05-28 13:46:57 +08:00
xream
532be2ff8c
Stash 正式版支持 VLESS REALITY(xtls-rprx-vision) 2025-05-27 19:46:31 +08:00
xream
37fc7ac88e
feat: VMess 支持 kcp/quic(正确处理 type, host, path, fp, alpn, tls等参数) 2025-05-27 03:01:28 +08:00
xream
9e0028219d
feat: Shadowrocket 支持 anytls 2025-05-26 17:24:39 +08:00
xream
54750d552b
feat: 为 env 响应增加如何使用前端搭配后端的引导说明 2025-05-25 00:58:06 +08:00
xream
0e7561a069
feat: Node.js 环境中 JSON 数据文件校验失败后会备份原文件, 创建新文件 2025-05-24 18:40:30 +08:00
xream
6804c6368a
fix: 修复 QX VLESS TLS 2025-05-23 22:36:08 +08:00
xream
9c5d6e9a10
feat: 单条订阅和文件支持链接参数 produceType raw, 此时返回原始数据的数组 2025-05-22 16:09:35 +08:00
xream
ef2d6be8eb
feat: 预处理支持 Base64 兜底 2025-05-22 15:17:38 +08:00
xream
04e12a4836
fix: 修复 SOCKS5 URI 2025-05-21 01:39:30 +08:00
xream
f94cf7185a
feat: 日志增加 body JSON limit 2025-05-20 21:16:30 +08:00
xream
fa7df51f8c
feat: Shadowrocket 支持前置代理. 补充 demo.js 说明 2025-05-18 17:21:54 +08:00
xream
18659d1cc8
feat: Node.js 环境下 API / 路由不自动跳转到 sub-store.vercel.app 2025-05-17 22:49:12 +08:00
xream
1d12dc55bd
feat: 单条订阅和文件支持链接参数 produceType raw, 此时返回原始数据的数组 2025-05-17 20:22:24 +08:00
xream
af9a2c86c1
fix: 修复 Surge/Loon VMess aead 2025-05-12 12:26:14 +08:00
xream
98892fa100
fix: 修复 QX VMess aead 2025-05-12 11:06:13 +08:00
xream
6e2411e2c2
doc: demo.js 2025-05-12 01:42:42 +08:00
xream
b3f6876bbd
feat: 兼容 xishang0128/sparkle 的 JavaScript 覆写; ProxyUtils 新增 Buffer, Base64 2025-05-11 16:21:28 +08:00
xream
d2c3956884
feat: QX 正式支持 SS2022 2025-05-07 02:59:19 +08:00
xream
21c1e11976
feat: 兼容非标 Shadowsocks URI 输入 2025-04-28 13:55:15 +08:00
xream
e0f6b3e692
feat: sing-box Hysteria up/down 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps 2025-04-28 10:23:30 +08:00
xream
0d2920fadd
feat: 兼容 Shadowrocket 非标 VMess URI 输入中的 peer(sni) 2025-04-27 09:45:14 +08:00
xream
da9b1d8795
feat: 输出到 Clash/Stash/Shadowrocket 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能 2025-04-26 13:46:58 +08:00
xream
4c4bda563a
feat: Stash 输出中过滤掉有前置代理的节点, 并在日志中提示 2025-04-22 09:42:32 +08:00
xream
95f181351a
feat: 忽略失败的远程选择支持开启通知(前端 >= 2.15.17) 2025-04-21 19:28:58 +08:00
xream
3b85063f73
feat: 简单实现了 SUB_STORE_MMDB_CRON 定时更新 MMDB. ASN: SUB_STORE_MMDB_ASN_PATH, SUB_STORE_MMDB_ASN_URL. COUNTRY: SUB_STORE_MMDB_COUNTRY_PATH, SUB_STORE_MMDB_COUNTRY_URL; 脚本中新增 ProxyUtils.downloadFile 方便下载二进制文件. 2025-04-21 18:25:00 +08:00
xream
7f691c8511
fix: SS 解析增加默认节点名 2025-04-20 20:10:08 +08:00
xream
55cc7dcd16
fix: 修复 URI 输出 2025-04-19 16:44:40 +08:00
xream
4f745b0232
feat: sing-box 输出支持 brutal 2025-04-18 22:49:19 +08:00
xream
28b233b62c
fix: 修复 URI 输出 2025-04-18 15:04:06 +08:00
xream
44d72523ce
feat: AnyTLS URI 支持 UDP 参数 2025-04-18 12:24:31 +08:00
xream
b60995f7ac
feat: Loon 输入输出正式支持 VLESS XTLS/REALITY, VMess REALITY 2025-04-17 09:57:56 +08:00
xream
a262dfbbe8
fix: 修复 Loon block-quic 参数 2025-04-16 07:28:28 +08:00
xream
166f3cb447
feat: 支持 QX udp-over-tcp=true/sp.v1/sp.v2 2025-04-14 15:39:13 +08:00
xream
1f0463bfe2
feat: 支持 QX udp-over-tcp=true/sp.v1; mihomo UDP over TCP 的协议版本默认 1, sing-box 默认为 2 2025-04-14 15:28:35 +08:00
xream
302c92ed87
fix: 修复 TUIC congestion-controller 2025-04-13 03:28:11 +08:00
xream
0d575e6e88
doc: demo.js 2025-04-11 22:49:12 +08:00
xream
d41b54abde
feat: 支持 Loon block-quic 参数 2025-04-11 22:44:12 +08:00
xream
2c3e701149
doc: demo.js 2025-04-11 15:21:41 +08:00
xream
b074f42fdc
feat: 拉取文件时 日志输出 User-Agent; 脚本上下文参数 $options 中新增 _req 字段, 包含请求信息 2025-04-08 12:48:38 +08:00
xream
e054b71a62
feat: Shadowrocket VMess ws 传输层增加默认 path 2025-04-03 22:33:31 +08:00
xream
7213cea16c
feat: Stash 正式版支持 SS2022, 测试版(>=3.1.0) 支持 VLESS REALITY(xtls-rprx-vision) 2025-04-03 15:47:35 +08:00
xream
260b1e5332
docs(README): 增加赞助商信息 2025-04-03 15:23:15 +08:00
xream
73e5d53f48
feat: Loon 输入输出支持 VLESS XTLS/REALITY, VMess REALITY. 需 includeUnsupportedProxy 或 build >= 842 自动开启) 2025-04-01 18:22:28 +08:00
xream
39829fa97a
feat: QX 输入值支持 = 2025-03-29 19:52:40 +08:00
xream
93d524331a
feat: QX 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2025-03-29 14:17:59 +08:00
xream
e0c6cc4453
feat: 正则排序支持顺序/倒序/原顺序(前端 > 2.15.10) 2025-03-28 12:46:51 +08:00
xream
80955aa339
doc: 标记 Clash Deprecated 2025-03-27 19:53:46 +08:00
xream
4d27e5bdac
feat: 脚本链接叠加参数调整 2025-03-27 12:52:19 +08:00
xream
e2011de69e
feat: Loon 解析器支持参数 resourceUrlOnly 仅使用远程资源, 忽略 Loon 自身解析数据 2025-03-26 00:26:31 +08:00
xream
9568f4d6d9
feat: 优化日志, Loon 解析器自动读取 build 2025-03-25 23:58:28 +08:00
xream
543641de9d
feat: VLESS 兼容 Shadowrocket 传输层 none 2025-03-25 23:35:23 +08:00
xream
2fbc589a8a
feat: Loon 输入输出支持 VLESS REALITY(flow 为 xtls-rprx-vision). 需 includeUnsupportedProxy 或 build >= 838 自动开启) 2025-03-25 22:22:29 +08:00
xream
c854614efc
feat: 调整 User-Agent 判断 2025-03-25 17:49:47 +08:00
xream
16a5995d21
fix: 修复 ss shadow-tls 2025-03-23 14:32:24 +08:00
xream
15b55f6d1a
feat: 更新文件时, 更新同步配置; 更新单条订阅/组合订阅时, 更新 mihomo 覆写 2025-03-21 00:36:42 +08:00
xream
8e5ce26e7b
fix: 修复重置后端数据后无默认字段的问题 2025-03-20 22:03:06 +08:00
Aritro37
c5d8aff73c
fix: 修复聚合模式下,名称带有中文或特殊符号的分享token判断异常的问题 2025-03-20 21:57:42 +08:00
xream
5696492dde
release: bump version 2025-03-19 16:11:49 +08:00
Aritro37
e6d05fd873
perf: 增加 MERGE 模式下的信息输出 2025-03-19 16:08:48 +08:00
Aritro37
4111b8fabf
fix: 修复 SUB_STORE_FRONTEND_PATH 使用绝对目录时前端资源 Content-Type 响应错误的问题 2025-03-19 15:52:37 +08:00
Aritro37
dfc619a181
feat: 引入SUB_STORE_BACKEND_MERGE 变量实现前后端端口合并及安全增强
1. 新增SUB_STORE_BACKEND_MERGE配置变量,支持功能整合模式:
   - 当设置SUB_STORE_BACKEND_MERGE为非空任意值时,后端支持同时处理API和前端资源请求
   - 新增配置示例:
     #合并前后端端口
     SUB_STORE_BACKEND_MERGE=true
     #设置接口安全地址
     SUB_STORE_FRONTEND_BACKEND_PATH=/safe-api
     #设置前端文件的路径
     SUB_STORE_FRONTEND_PATH=./dist
     #后端监听的端口
     SUB_STORE_BACKEND_API_PORT=3000
     #后端监听的HOST
     SUB_STORE_BACKEND_API_HOST="127.0.0.1"

2. 合并后支持前端在子路由界面刷新:
   - 原前端在subs、files、sync等页面刷新时会出现404问题,合并后修复了该问题
2025-03-19 15:26:17 +08:00
Aritro37
ff5283a66f
fix: 修复使用 .env 时 /api/utils/env 接口中的 env 字段为空的问题 2025-03-19 15:07:01 +08:00
xream
6c54518e84
chore: 日志 2025-03-18 13:34:50 +08:00
Aritro37
dd92a26e6c
Perf: 提前加载 .env;后端复用前端 Path 2025-03-17 22:01:38 +08:00
xream
bb5c9d43d0
Merge pull request #430 from Aritro37/master
feat: 支持通过.env配置环境变量,后端支持设置前置路由
2025-03-17 17:07:37 +08:00
Aritro37
e54ac92357
feat: 支持通过.env配置环境变量,后端支持设置前置路由 2025-03-17 17:04:24 +08:00
xream
507e37021c
feat: 增加更多的同步配置日志 2025-03-16 15:48:47 +08:00
xream
a70dc7b913
feat: undici 配置重定向 2025-03-15 22:50:47 +08:00
xream
fc56df7bfd
fix: 处理 YAML short-idnull 的情况 2025-03-15 16:29:19 +08:00
xream
1281df59f3
feat: 增强 VMess URI 解析兼容性; 修改导出文件名格式 2025-03-13 20:19:45 +08:00
xream
1faa3fb793
fix: 修复 VMess URI IPv6 格式 2025-03-13 19:26:53 +08:00
xream
47307716b2
feat: url 支持 credentials; 修改导出文件名格式 2025-03-13 17:42:48 +08:00
xream
312caa6880
feat: patch http-proxy; 使用 undici 替代 request 2025-03-13 13:02:19 +08:00
xream
15a51e0dd0
fix: 修复文件预览未使用代理策略的问题 2025-03-12 19:44:54 +08:00
xream
8116c78dda
feat: 升级 http-proxy-middleware 2025-03-12 15:30:14 +08:00
xream
6a026a3d07
feat: mihomo hysteria2 兼容 obfs_password 字段 2025-03-10 23:06:33 +08:00
xream
cef931fa5d
feat: Hysteria2 URI 输入输出支持 hop-interval 和 keepalive 参数, 为保证兼容性, 输出时多端口暂时保持使用 mport 参数 2025-03-10 19:52:15 +08:00
xream
29525b3e22
feat: sing-box hop_interval 和 server_ports 不需要 includeUnsupportedProxy 2025-03-10 19:36:41 +08:00
xream
8f701570e4
feat: Stash 使用 includeUnsupportedProxy 参数开启 XTLS-uTLS-Vision-REALITY(版本>=2.8.0 时自动开启) 2025-03-07 14:09:56 +08:00
xream
3f8269e835
feat: Node.js 环境支持自定义 JSON Body limit, 例: SUB_STORE_BODY_JSON_LIMIT=10mb 2025-03-07 13:59:22 +08:00
xream
465b62218a
feat: 验证 mihomo ss cipher 2025-03-05 15:25:16 +08:00
xream
d255390d48
fix: 修复 Surge shadow-tls-password 引号解析 2025-03-04 22:36:47 +08:00
xream
72c7f4333a
feat: SurgeMac mihomo 配置中支持自定义 DNS 2025-03-04 20:11:22 +08:00
xream
f35837ff9f
feat: 支持 AnyTLS URI 2025-03-03 20:52:31 +08:00
xream
c2c39c5de6
fix: 修复 Egern 输出 2025-03-03 10:42:44 +08:00
xream
87a4b14ae2
feat(wip): 本地脚本支持传入参数 2025-03-02 19:02:15 +08:00
xream
ff1dacda87
区域过滤和协议过滤支持保留模式和过滤模式(后端需 >= 2.17.0, 前端需 >= 2.15.0) 2025-03-02 11:06:33 +08:00
xream
9426f128c4
feat: Surge 输出会判断 HTTP 是否 headers 字段 2025-03-01 21:43:14 +08:00
xream
ebc7173c95
feat: 文件类型为 mihomo 配置时, 来源可以为无 2025-03-01 08:45:17 +08:00
xream
dd4e0cef68
feat: 扩展 scriptResourceCache 缓存, 详见 demo.js 2025-02-28 15:54:04 +08:00
xream
b1618c3803
feat: 支持使用环境变量 SUB_STORE_PRODUCE_CRON 在后台定时处理订阅, 格式为 0 */2 * * *,sub,a;0 */3 * * *,col,b 2025-02-28 14:07:35 +08:00
xream
1b4c046b75
fix: mihomo 覆写可以多次使用 2025-02-27 23:37:39 +08:00
xream
41034ceb46
feat: 规范化 subscription-userinfo 2025-02-27 23:23:32 +08:00
xream
6efb19c856
feat: geo 更新 2025-02-27 17:27:15 +08:00
xream
2cd30dfe68
feat: 内容无变化时 不进行上传; 增加 gist 数量日志 2025-02-26 18:50:11 +08:00
xream
d53947d820
feat: sing-box 支持 anytls 2025-02-23 09:48:09 +08:00
xream
7e75031e92
fix: 修复 short-id 正则 2025-02-22 14:25:06 +08:00
xream
4a07c02dc1
feat: 支持 Shadowrocket Shadowsocks 输入中的 Shadow TLS 参数 2025-02-21 01:44:34 +08:00
xream
95d6688539
fix: 修复 Shadowrocket 输出的 Shadow TLS 2025-02-21 00:50:38 +08:00
xream
a23e2ffcd6
fix: uuid 只辅助判断, 不直接过滤 2025-02-20 22:52:35 +08:00
xream
fda1252d0e
fix: 修复 Egern http 传输层 2025-02-20 22:24:39 +08:00
xream
62c5c2e15b
fix: 修复 Loon ip-mode 2025-02-19 17:15:31 +08:00
xream
ffabcc9391
feat: 支持 anytls 协议 2025-02-19 17:01:40 +08:00
xream
0825f15d04
feat: Egern 支持 Shadow TLS 2025-02-18 15:07:24 +08:00
xream
fbf6b5ce6e
fix: UUID 2025-02-16 05:05:33 +08:00
xream
3eb0816c88
fix: 修复 TUIC URI 2025-02-15 20:47:34 +08:00
xream
8fc755ff02
fix: 文件类型为 mihomo 配置时, 不应处理本地或远程内容字段 2025-02-15 20:32:29 +08:00
xream
6d3d6fa1b3
feat: 仅匹配 UUIDv4 2025-02-15 19:58:34 +08:00
xream
4ef4431c2c
feat: 兼容更多 TUIC URI 字段 2025-02-14 23:27:01 +08:00
xream
5058662651
feat: 下载文件名增加前后缀 2025-02-14 15:39:13 +08:00
xream
f9d120bac3
feat: 兼容 v2rayN 非标 TUIC URI 2025-02-13 20:26:59 +08:00
xream
72a445ae33
doc: README 2025-02-12 22:39:18 +08:00
xream
5e2a87e250
fix: 修复 Shadowsocks URI 解析 2025-02-12 19:21:24 +08:00
xream
71fc9affbf
feat: 支持 v2ray SOCKS URI 的输入和输出 2025-02-12 03:27:40 +08:00
xream
6f82294c49
fix: 修复 Egern VMess tcp 2025-02-11 23:56:45 +08:00
xream
7c398ba51c
fix: 修复 mihomo 覆写配置无法使用普通脚本的问题 2025-02-11 13:18:42 +08:00
xream
7002eee88d
feat: 调整 Egern VMess 传输层 2025-02-10 21:02:40 +08:00
xream
bd21d58fe7
feat: VMess/VLESS 校验 uuid 2025-02-10 13:34:58 +08:00
xream
2ea46dcbf1
feat: Shadowsocks URI 部分逻辑修正 2025-02-10 06:44:24 +08:00
xream
4a2a2297f6
feat: Shadowsocks URI 支持 Shadow TLS plugin 2025-02-10 06:32:17 +08:00
xream
07d5a913f0
feat: 同步配置逻辑优化 2025-02-09 20:58:27 +08:00
xream
421df8f0d4
doc: README 2025-02-07 19:43:06 +08:00
xream
e14944dd19
feat: 调整 Egern VMess security 逻辑 2025-02-06 18:18:15 +08:00
xream
bf18c51f6a
feat: mihomo 和 Shadowrocket VMess cipher 支持 zero 2025-02-06 18:08:46 +08:00
xream
23e8fbd1b7
feat: Proxy URI Scheme 支持省略端口号(http 默认为 80, tls 默认为 443) 2025-02-06 14:59:50 +08:00
xream
b94b3c366b
feat: Egern 正式支持 Shadowsocks 2022 2025-02-06 00:04:54 +08:00
xream
afb5f7b880
feat: 支持 VLESS spx 参数; 支持 Trojan 结合 REALITY/XHTTP 2025-02-05 20:01:41 +08:00
xream
74ec133a79
feat: Loon 正式支持 Shadow-TLS 2025-02-03 13:47:17 +08:00
xream
2a76eb6462
feat: mihomo snell 版本小于 3 的节点, 强制去除 udp 字段, 防止内核报错 2025-02-02 18:59:14 +08:00
xream
9ac5e136a6
feat: 去除订阅流量信息中空字段, 增强兼容性 2025-02-02 18:39:46 +08:00
xream
38f5a97a20
fix: 修复 Surge 输入的 tfo 2025-01-31 15:14:19 +08:00
xream
14a3488ce2
fix: 修复 Egern 和 Stash 可根据 User-Agent 自动包含官方/商店版/未续费订阅不支持的协议 2025-01-26 20:41:57 +08:00
xream
6afec4f668
feat: Egern 增加 TUIC 2025-01-23 08:22:48 +08:00
xream
b1874e510d
feat: 支持 VLESS XHTTP extra 2025-01-22 09:43:43 +08:00
xream
48aaaf5c99
doc: README 2025-01-21 12:02:49 +08:00
xream
7385e17a4c
fix: 修复 Base64 合法性判断 2025-01-17 16:34:30 +08:00
xream
c3daea55ab
feat: Loon 节点支持 ip-mode 2025-01-15 23:54:35 +08:00
xream
fc9ff48b1f fix: ss none 必须配置 password 2025-01-15 23:11:34 +08:00
xream
fb21890b68 fix: 修复组合订阅空 subscription-userinfo 的问题 2025-01-14 11:34:02 +08:00
xream
2155cc9639 fix: 修复组合订阅中的单条订阅透传 User-Agent 2025-01-14 08:25:53 +08:00
xream
03e320cbd0 feat: 组合订阅中的单条订阅也支持透传 User-Agent 2025-01-13 20:09:00 +08:00
xream
e325b9a39a feat: Loon 排除 XTLS; 切换使用 esbuild 打包 2025-01-13 16:03:52 +08:00
xream
87597f6fc2 ci: pnpm 2025-01-13 14:44:34 +08:00
xream
3462d36c35 feat: Egern 和 Stash 可根据 User-Agent 自动包含官方/商店版/未续费订阅不支持的协议 2025-01-13 14:27:08 +08:00
xream
02946ec81c feat: Surge 默认开启 Shadowsocks 2022 2025-01-13 14:00:38 +08:00
xream
c963c872ff feat: Egern 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2025-01-12 06:12:14 +08:00
xream
c4a1bb4ea1 feat: Loon 使用 includeUnsupportedProxy 参数开启 Shadowsocks/ShadowsocksR + Shadow TLS 2025-01-11 23:34:00 +08:00
xream
f96d9dea74 feat: 日志中增加上传配置的响应 2025-01-09 18:56:36 +08:00
xream
01eb69d8ae ci: GitHub Action 2025-01-09 09:35:07 +08:00
xream
797ba6f601 fix: 修复 Loon Shadow TLS 2025-01-09 09:30:16 +08:00
xream
128353a7f3 feat: gist 单页数量改为 100 2025-01-09 09:14:36 +08:00
xream
e6f6d51608 feat: Loon 使用 includeUnsupportedProxy 参数开启 Shadowsocks + Shadow TLS V3 2025-01-08 22:52:00 +08:00
xream
589a6bfadb feat: Base64 Pre-processor 检测解码是否正常 2025-01-08 20:13:46 +08:00
xream
75012503f8 fix: 修复 Clash Pre-processor 2025-01-08 19:49:34 +08:00
xream
85a3e2ee54
feat: 文件支持 Mihomo 配置, 支持使用覆写; target 名称适配大小写和别名 2025-01-03 23:37:50 +08:00
xream
95b7557635
feat: Loon 正式支持 Shadowsocks 2022 2024-12-31 23:24:35 +08:00
xream
14ca62db4a
doc: demo.js 2024-12-31 15:28:32 +08:00
xream
a2a754adb7
feat: sing-box 支持使用 _network 字段来设置 network 2024-12-31 15:27:14 +08:00
xream
6b23f82953
fix: 修复代理 App 版中路由 target 参数为空的情况 2024-12-30 21:36:45 +08:00
xream
e071a7f253
feat: 组合订阅和文件的导出导入 2024-12-30 21:21:18 +08:00
xream
b9bba895e1
feat: 支持订阅级别的 noCache 2024-12-29 23:37:07 +08:00
xream
8090d678ee
feat: 分享支持多一级路由指定输出目标 2024-12-29 22:08:24 +08:00
xream
ff4be7ac38
feat: 订阅支持开关 passThroughUA 透传请求的 User-Agent 2024-12-29 21:33:15 +08:00
xream
7e2109dc68
feat: 支持订阅参数 url 同时支持单条本地节点内容, 支持多一级路由指定输出目标 2024-12-29 21:03:52 +08:00
xream
278beae99a
feat: 支持 Egern 前置代理 prev_hop 和 Hysteria2 端口跳跃 2024-12-29 20:05:55 +08:00
56 changed files with 9226 additions and 8067 deletions

View File

@ -1,5 +1,6 @@
name: build name: build
on: on:
workflow_dispatch:
push: push:
branches: branches:
- master - master
@ -26,18 +27,18 @@ jobs:
run: | run: |
npm install -g pnpm npm install -g pnpm
cd backend && pnpm i --no-frozen-lockfile cd backend && pnpm i --no-frozen-lockfile
- name: Test # - name: Test
run: | # run: |
cd backend # cd backend
pnpm test # pnpm test
- name: Build # - name: Build
run: | # run: |
cd backend # cd backend
pnpm run build # pnpm run build
- name: Bundle - name: Bundle
run: | run: |
cd backend cd backend
pnpm run bundle pnpm bundle:esbuild
- id: tag - id: tag
name: Generate release tag name: Generate release tag
run: | run: |

View File

@ -26,21 +26,24 @@ Core functionalities:
### Supported Input Formats ### Supported Input Formats
> ⚠️ Do not use `Shadowrocket` to export URI and then import it as input. It is not a standard URI. > ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs.
- [x] Normal Proxy(`socks5`, `socks5+tls`, `http`, `https`(it's ok)) - [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
example: `socks5+tls://user:pass@ip:port#name` example: `socks5+tls://user:pass@ip:port#name`
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard) - [x] URI(AnyTLS, SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
- [x] Clash Proxies YAML - [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line) - [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge)) - [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard)) - [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru) - [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH) - [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard) - [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms ### Supported Target Platforms
@ -48,7 +51,6 @@ Core functionalities:
- [x] Plain JSON - [x] Plain JSON
- [x] Stash - [x] Stash
- [x] Clash.Meta(mihomo) - [x] Clash.Meta(mihomo)
- [x] Clash
- [x] Surfboard - [x] Surfboard
- [x] Surge - [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself) - [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
@ -60,6 +62,10 @@ Core functionalities:
- [x] V2Ray - [x] V2Ray
- [x] V2Ray URI - [x] V2Ray URI
Deprecated:
- [x] Clash
## 2. Subscription Formatting ## 2. Subscription Formatting
### Filtering ### Filtering
@ -122,3 +128,9 @@ This project is under the GPL V3 LICENSE.
- 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!
- Special thanks to @Orz-3 and @58xinian for their awesome icons. - Special thanks to @Orz-3 and @58xinian for their awesome icons.
## Sponsors
[![image](./support.nodeseek.com_page_promotion_id=8.png)](https://yxvm.com)
[NodeSupport](https://github.com/NodeSeekDev/NodeSupport) sponsored this project.

View File

@ -5,7 +5,7 @@
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝ * ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗ * ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ * ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket! * Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket!
* @updated: <%= updated %> * @updated: <%= updated %>
* @version: <%= pkg.version %> * @version: <%= pkg.version %>
* @author: Peng-YM * @author: Peng-YM

View File

@ -1,7 +1,7 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.15.5", "version": "2.19.60",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@ -12,28 +12,37 @@
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js", "dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
"build": "gulp", "build": "gulp",
"bundle": "node bundle.js", "bundle": "node bundle.js",
"bundle:esbuild": "node bundle-esbuild.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s" "changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
}, },
"author": "Peng-YM", "author": "Peng-YM",
"license": "GPL-3.0", "license": "GPL-3.0",
"pnpm": {
"patchedDependencies": {
"http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch"
}
},
"dependencies": { "dependencies": {
"@maxmind/geoip2-node": "^5.0.0", "@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7", "automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dotenv": "^16.4.7",
"connect-history-api-fallback": "^2.0.0", "connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6", "cron": "^3.1.6",
"dns-packet": "^5.6.1", "dns-packet": "^5.6.1",
"express": "^4.17.1", "express": "^4.17.1",
"http-proxy-middleware": "^2.0.6", "mime-types": "^2.1.35",
"http-proxy-middleware": "^3.0.3",
"ip-address": "^9.0.5", "ip-address": "^9.0.5",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",
"jsrsasign": "^11.1.0", "jsrsasign": "^11.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"ms": "^2.1.3", "ms": "^2.1.3",
"nanoid": "^3.3.3", "nanoid": "^3.3.3",
"request": "^2.88.2", "semver": "^7.6.3",
"static-js-yaml": "^1.0.0" "static-js-yaml": "^1.0.0",
"undici": "^7.4.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.0", "@babel/core": "^7.18.0",

View File

@ -0,0 +1,46 @@
diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js
index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644
--- a/lib/http-proxy/common.js
+++ b/lib/http-proxy/common.js
@@ -1,6 +1,5 @@
var common = exports,
url = require('url'),
- extend = require('util')._extend,
required = require('requires-port');
var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i,
@@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) {
);
outgoing.method = options.method || req.method;
- outgoing.headers = extend({}, req.headers);
+ outgoing.headers = Object.assign({}, req.headers);
if (options.headers){
- extend(outgoing.headers, options.headers);
+ Object.assign(outgoing.headers, options.headers);
}
if (options.auth) {
diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js
index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644
--- a/lib/http-proxy/index.js
+++ b/lib/http-proxy/index.js
@@ -1,5 +1,4 @@
var httpProxy = module.exports,
- extend = require('util')._extend,
parse_url = require('url').parse,
EE3 = require('eventemitter3'),
http = require('http'),
@@ -47,9 +46,9 @@ function createRightProxy(type) {
args[cntr] !== res
) {
//Copy global options
- requestOptions = extend({}, options);
+ requestOptions = Object.assign({}, options);
//Overwrite with request options
- extend(requestOptions, args[cntr]);
+ Object.assign(requestOptions, args[cntr]);
cntr--;
}

13757
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,13 @@
import { Base64 } from 'js-base64';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import rs from '@/utils/rs'; import rs from '@/utils/rs';
import YAML from '@/utils/yaml'; import YAML from '@/utils/yaml';
import download from '@/utils/download'; import download, { downloadFile } from '@/utils/download';
import { import {
isIPv4, isIPv4,
isIPv6, isIPv6,
isValidPortNumber, isValidPortNumber,
isValidUUID,
isNotBlank, isNotBlank,
ipAddress, ipAddress,
getRandomPort, getRandomPort,
@ -21,6 +23,8 @@ import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo'; import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
import Gist from '@/utils/gist'; import Gist from '@/utils/gist';
import { isPresent } from './producers/utils';
import { doh } from '@/utils/dns';
function preprocess(raw) { function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) { for (const processor of PROXY_PREPROCESSORS) {
@ -75,7 +79,16 @@ function parse(raw) {
$.error(`Failed to parse line: ${line}`); $.error(`Failed to parse line: ${line}`);
} }
} }
return proxies; return proxies.filter((proxy) => {
if (['vless', 'vmess'].includes(proxy.type)) {
const isProxyUUIDValid = isValidUUID(proxy.uuid);
if (!isProxyUUIDValid) {
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
}
// return isProxyUUIDValid;
}
return true;
});
} }
async function processFn( async function processFn(
@ -102,12 +115,7 @@ async function processFn(
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') {
let noCache;
let url = content || ''; 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) {
@ -126,10 +134,17 @@ async function processFn(
} }
} }
} }
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`; url = `${url.split('#')[0]}${
const downloadUrlMatch = url.match( rawArgs[2]
/^\/api\/(file|module)\/(.+)/, ? `#${rawArgs[2]}`
); : $arguments?.noCache != null ||
$arguments?.insecure != null
? `#${rawArgs[1]}`
: ''
}`;
const downloadUrlMatch = url
.split('#')[0]
.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) { if (downloadUrlMatch) {
let type = ''; let type = '';
try { try {
@ -159,6 +174,17 @@ async function processFn(
); );
throw new Error(`无法加载 ${type}: ${url}`); throw new Error(`无法加载 ${type}: ${url}`);
} }
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
script = fs.readFileSync(url.split('#')[0], 'utf8');
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when reading local script: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取脚本文件: ${url}`);
}
} else { } else {
// if this is a remote script, download it // if this is a remote script, download it
try { try {
@ -173,6 +199,7 @@ async function processFn(
} }
} else { } else {
script = content; script = content;
$arguments = item.args.arguments || {};
} }
} }
@ -214,10 +241,22 @@ function produce(proxies, targetPlatform, type, opts = {}) {
); );
// filter unsupported proxies // filter unsupported proxies
proxies = proxies.filter( proxies = proxies.filter((proxy) => {
(proxy) => // 检查代理是否支持目标平台
!(proxy.supported && proxy.supported[targetPlatform] === false), if (proxy.supported && proxy.supported[targetPlatform] === false) {
); return false;
}
// 对于 vless 和 vmess 代理,需要额外验证 UUID
if (['vless', 'vmess'].includes(proxy.type)) {
const isProxyUUIDValid = isValidUUID(proxy.uuid);
if (!isProxyUUIDValid)
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
// return isProxyUUIDValid;
}
return true;
});
proxies = proxies.map((proxy) => { proxies = proxies.map((proxy) => {
proxy._resolved = proxy.resolved; proxy._resolved = proxy.resolved;
@ -303,6 +342,11 @@ export const ProxyUtils = {
MMDB, MMDB,
Gist, Gist,
download, download,
downloadFile,
isValidUUID,
doh,
Buffer,
Base64,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@ -343,6 +387,14 @@ function lastParse(proxy) {
if (typeof proxy.password === 'number') { if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password); proxy.password = numberToString(proxy.password);
} }
if (
['ss'].includes(proxy.type) &&
proxy.cipher === 'none' &&
!proxy.password
) {
// https://github.com/MetaCubeX/mihomo/issues/1677
proxy.password = '';
}
if (proxy.interface) { if (proxy.interface) {
proxy['interface-name'] = proxy.interface; proxy['interface-name'] = proxy.interface;
delete proxy.interface; delete proxy.interface;
@ -392,9 +444,14 @@ function lastParse(proxy) {
} }
} }
if ( if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes( [
proxy.type, 'trojan',
) 'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) { ) {
proxy.tls = true; proxy.tls = true;
} }
@ -478,6 +535,14 @@ function lastParse(proxy) {
proxy['obfs-password'] = proxy.obfs; proxy['obfs-password'] = proxy.obfs;
proxy.obfs = 'salamander'; proxy.obfs = 'salamander';
} }
if (
['hysteria2'].includes(proxy.type) &&
!proxy['obfs-password'] &&
proxy['obfs_password']
) {
proxy['obfs-password'] = proxy['obfs_password'];
delete proxy['obfs_password'];
}
if (['vless'].includes(proxy.type)) { if (['vless'].includes(proxy.type)) {
// 删除 reality-opts: {} // 删除 reality-opts: {}
if ( if (
@ -564,6 +629,20 @@ function lastParse(proxy) {
if (!proxy['tls-fingerprint'] && caStr) { if (!proxy['tls-fingerprint'] && caStr) {
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr); proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
} }
if (
['ss'].includes(proxy.type) &&
isPresent(proxy, 'shadow-tls-password')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-version'];
}
return proxy; return proxy;
} }

View File

@ -11,6 +11,7 @@ import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon'; import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx'; import getQXParser from './peggy/qx';
import getTrojanURIParser from './peggy/trojan-uri'; import getTrojanURIParser from './peggy/trojan-uri';
import $ from '@/core/app';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
@ -40,8 +41,21 @@ function URI_PROXY() {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, type, tls, username, password, server, port, query, name] = let [__, type, tls, username, password, server, port, query, name] =
line.match( line.match(
/^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?):(\d+?)(\?.*?)?(?:#(.*?))?$/, /^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
); );
if (port) {
port = parseInt(port, 10);
} else {
if (tls) {
port = 443;
} else if (type === 'http') {
port = 80;
} else {
$.error(`port is not present in line: ${line}`);
throw new Error(`port is not present in line: ${line}`);
}
$.info(`port is not present in line: ${line}, set to ${port}`);
}
const proxy = { const proxy = {
name: name:
@ -62,7 +76,46 @@ function URI_PROXY() {
}; };
return { name, test, parse }; return { name, test, parse };
} }
function URI_SOCKS() {
const name = 'URI SOCKS Parser';
const test = (line) => {
return /^socks:\/\//.test(line);
};
const parse = (line) => {
// parse url
// eslint-disable-next-line no-unused-vars
let [__, type, auth, server, port, query, name] = line.match(
/^(socks)?:\/\/(?:(.*)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${line}`);
throw new Error(`port is not present in line: ${line}`);
}
let username, password;
if (auth) {
const parsed = Base64.decode(decodeURIComponent(auth)).split(':');
username = parsed[0];
password = parsed[1];
}
const proxy = {
name:
name != null
? decodeURIComponent(name)
: `${type} ${server}:${port}`,
type: 'socks5',
server,
port,
username,
password,
};
return proxy;
};
return { name, test, parse };
}
// Parse SS URI format (only supports new SIP002, legacy format is depreciated). // Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme // reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() { function URI_SS() {
@ -75,13 +128,13 @@ function URI_SS() {
// parse url // parse url
let content = line.split('ss://')[1]; let content = line.split('ss://')[1];
let name = line.split('#')[1];
const proxy = { const proxy = {
name: decodeURIComponent(line.split('#')[1]),
type: 'ss', type: 'ss',
}; };
content = content.split('#')[0]; // strip proxy name content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6 // handle IPV4 and IPV6
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/); let serverAndPortArray = content.match(/@([^/?]*)(\/|\?|$)/);
let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大 let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大
let userInfoStr; let userInfoStr;
@ -99,6 +152,7 @@ function URI_SS() {
query = parsed[2]; query = parsed[2];
} }
content = Base64.decode(content); content = Base64.decode(content);
if (query) { if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) { if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/); const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
@ -112,8 +166,11 @@ function URI_SS() {
} }
content = `${content}${query}`; content = `${content}${query}`;
} }
userInfoStr = content.split('@')[0]; userInfoStr = content.match(/(^.*)@/)?.[1];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/); serverAndPortArray = content.match(/@([^/@]*)(\/|$)/);
} else if (content.includes('?')) {
const parsed = content.match(/(\?.*)$/);
query = parsed[1];
} }
const serverAndPort = serverAndPortArray[1]; const serverAndPort = serverAndPortArray[1];
@ -132,11 +189,12 @@ function URI_SS() {
// } // }
// handle obfs // handle obfs
const idx = content.indexOf('?plugin='); const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
if (idx !== -1) { const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
if (pluginMatch) {
const pluginInfo = ( const pluginInfo = (
'plugin=' + 'plugin=' + decodeURIComponent(pluginMatch[1])
decodeURIComponent(content.split('?plugin=')[1].split('&')[0])
).split(';'); ).split(';');
const params = {}; const params = {};
for (const item of pluginInfo) { for (const item of pluginInfo) {
@ -161,18 +219,51 @@ function URI_SS() {
tls: getIfPresent(params.tls), tls: getIfPresent(params.tls),
}; };
break; break;
case 'shadow-tls': {
proxy.plugin = 'shadow-tls';
const version = getIfNotBlank(params['version']);
proxy['plugin-opts'] = {
host: getIfNotBlank(params['host']),
password: getIfNotBlank(params['password']),
version: version ? parseInt(version, 10) : undefined,
};
break;
}
default: default:
throw new Error( throw new Error(
`Unsupported plugin option: ${params.plugin}`, `Unsupported plugin option: ${params.plugin}`,
); );
} }
} }
// Shadowrocket
if (shadowTlsMatch) {
const params = JSON.parse(Base64.decode(shadowTlsMatch[1]));
const version = getIfNotBlank(params['version']);
const address = getIfNotBlank(params['address']);
const port = getIfNotBlank(params['port']);
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: getIfNotBlank(params['host']),
password: getIfNotBlank(params['password']),
version: version ? parseInt(version, 10) : undefined,
};
if (address) {
proxy.server = address;
}
if (port) {
proxy.port = parseInt(port, 10);
}
}
if (/(&|\?)uot=(1|true)/i.test(query)) { if (/(&|\?)uot=(1|true)/i.test(query)) {
proxy['udp-over-tcp'] = true; proxy['udp-over-tcp'] = true;
} }
if (/(&|\?)tfo=(1|true)/i.test(query)) { if (/(&|\?)tfo=(1|true)/i.test(query)) {
proxy.tfo = true; proxy.tfo = true;
} }
if (name != null) {
name = decodeURIComponent(name);
}
proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`;
return proxy; return proxy;
}; };
return { name, test, parse }; return { name, test, parse };
@ -255,7 +346,7 @@ function URI_VMess() {
}; };
const parse = (line) => { const parse = (line) => {
line = line.split('vmess://')[1]; line = line.split('vmess://')[1];
let content = Base64.decode(line); let content = Base64.decode(line.replace(/\?.*?$/, ''));
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());
@ -348,7 +439,16 @@ function URI_VMess() {
type: 'vmess', type: 'vmess',
server, server,
port, port,
cipher: getIfPresent(params.scy, 'auto'), // https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link
// https://github.com/XTLS/Xray-core/issues/91
cipher: [
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(params.scy)
? params.scy
: 'auto',
uuid: params.id, uuid: params.id,
alterId: parseInt( alterId: parseInt(
getIfPresent(params.aid ?? params.alterId, 0), getIfPresent(params.aid ?? params.alterId, 0),
@ -365,8 +465,12 @@ function URI_VMess() {
); );
} }
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && params.sni && params.sni !== '') { if (proxy.tls) {
if (params.sni && params.sni !== '') {
proxy.sni = params.sni; proxy.sni = params.sni;
} else if (params.peer && params.peer !== '') {
proxy.sni = params.peer;
}
} }
let httpupgrade = false; let httpupgrade = false;
// handle obfs // handle obfs
@ -378,8 +482,8 @@ function URI_VMess() {
['http'].includes(params.type) ['http'].includes(params.type)
) { ) {
proxy.network = 'http'; proxy.network = 'http';
} else if (['grpc'].includes(params.net)) { } else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
proxy.network = 'grpc'; proxy.network = params.net;
} else if ( } else if (
params.net === 'httpupgrade' || params.net === 'httpupgrade' ||
proxy.network === 'httpupgrade' proxy.network === 'httpupgrade'
@ -405,6 +509,11 @@ function URI_VMess() {
} catch (e) {} } catch (e) {}
let transportPath = params.path; let transportPath = params.path;
// 补上默认 path
if (['ws'].includes(proxy.network)) {
transportPath = transportPath || '/';
}
if (proxy.network === 'http') { if (proxy.network === 'http') {
if (transportHost) { if (transportHost) {
// 1)http(tcp)->host中间逗号(,)隔开 // 1)http(tcp)->host中间逗号(,)隔开
@ -424,13 +533,28 @@ function URI_VMess() {
} }
} }
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点 // 传输层应该有配置, 暂时不考虑兼容不给配置的节点
if (transportPath || transportHost) { if (
transportPath ||
transportHost ||
['kcp', 'quic'].includes(proxy.network)
) {
if (['grpc'].includes(proxy.network)) { if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = { proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath), 'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type), '_grpc-type': getIfNotBlank(params.type),
'_grpc-authority': getIfNotBlank(params.authority), '_grpc-authority': getIfNotBlank(params.authority),
}; };
} else if (['kcp', 'quic'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
[`_${proxy.network}-type`]: getIfNotBlank(
params.type,
),
[`_${proxy.network}-host`]: getIfNotBlank(
getIfNotBlank(transportHost),
),
[`_${proxy.network}-path`]:
getIfNotBlank(transportPath),
};
} else { } else {
const opts = { const opts = {
path: getIfNotBlank(transportPath), path: getIfNotBlank(transportPath),
@ -446,6 +570,12 @@ function URI_VMess() {
delete proxy.network; delete proxy.network;
} }
} }
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
// 然而 wiki 和 app 实测中都没有字段表示这个
// proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
return proxy; return proxy;
} }
}; };
@ -529,6 +659,9 @@ function URI_VLESS() {
if (params.sid) { if (params.sid) {
opts['short-id'] = params.sid; opts['short-id'] = params.sid;
} }
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (Object.keys(opts).length > 0) { if (Object.keys(opts).length > 0) {
// proxy[`${params.security}-opts`] = opts; // proxy[`${params.security}-opts`] = opts;
proxy[`${params.security}-opts`] = opts; proxy[`${params.security}-opts`] = opts;
@ -544,6 +677,9 @@ function URI_VLESS() {
} }
if (!proxy.network && isShadowrocket && params.obfs) { if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs; proxy.network = params.obfs;
if (['none'].includes(proxy.network)) {
proxy.network = 'tcp';
}
} }
if (['websocket'].includes(proxy.network)) { if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws'; proxy.network = 'ws';
@ -596,6 +732,61 @@ function URI_VLESS() {
// mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none即不使用伪装头部但不可以为空字符串。 // mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none即不使用伪装头部但不可以为空字符串。
proxy.headerType = params.headerType || 'none'; proxy.headerType = params.headerType || 'none';
} }
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
}
return proxy;
};
return { name, test, parse };
}
function URI_AnyTLS() {
const name = 'URI AnyTLS Parser';
const test = (line) => {
return /^anytls:\/\//.test(line);
};
const parse = (line) => {
line = line.split(/anytls:\/\//)[1];
// eslint-disable-next-line no-unused-vars
let [__, password, server, port, addons = '', name] =
/^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
password = decodeURIComponent(password);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `AnyTLS ${server}:${port}`;
const proxy = {
type: 'anytls',
name,
server,
port,
password,
};
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/g, '-');
value = decodeURIComponent(value);
if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined;
} else if (['insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
} else if (['udp'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else {
proxy[key] = value;
}
} }
return proxy; return proxy;
@ -632,6 +823,7 @@ function URI_Hysteria2() {
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec( ] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
line, line,
); );
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
if (/^\d+$/.test(port)) { if (/^\d+$/.test(port)) {
port = parseInt(`${port}`, 10); port = parseInt(`${port}`, 10);
@ -675,12 +867,23 @@ function URI_Hysteria2() {
if (params.obfs && params.obfs !== 'none') { if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs; proxy.obfs = params.obfs;
} }
if (params.mport) {
proxy.ports = params.mport; proxy.ports = params.mport;
}
proxy['obfs-password'] = params['obfs-password']; proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure); proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen); proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256; proxy['tls-fingerprint'] = params.pinSHA256;
let hop_interval = params['hop-interval'] || params['hop_interval'];
if (/^\d+$/.test(hop_interval)) {
proxy['hop-interval'] = parseInt(`${hop_interval}`, 10);
}
let keepalive = params['keepalive'];
if (/^\d+$/.test(keepalive)) {
proxy['keepalive'] = parseInt(`${keepalive}`, 10);
}
return proxy; return proxy;
}; };
@ -763,8 +966,11 @@ function URI_TUIC() {
const parse = (line) => { const parse = (line) => {
line = line.split(/tuic:\/\//)[1]; line = line.split(/tuic:\/\//)[1];
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
let [__, uuid, password, server, ___, port, ____, addons = '', name] = let [__, auth, server, port, addons = '', name] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); /^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
auth = decodeURIComponent(auth);
let [uuid, ...passwordParts] = auth.split(':');
let password = passwordParts.join(':');
port = parseInt(`${port}`, 10); port = parseInt(`${port}`, 10);
if (isNaN(port)) { if (isNaN(port)) {
port = 443; port = 443;
@ -786,14 +992,19 @@ function URI_TUIC() {
for (const addon of addons.split('&')) { for (const addon of addons.split('&')) {
let [key, value] = addon.split('='); let [key, value] = addon.split('=');
key = key.replace(/_/, '-'); key = key.replace(/_/g, '-');
value = decodeURIComponent(value); value = decodeURIComponent(value);
if (['alpn'].includes(key)) { if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined; proxy[key] = value ? value.split(',') : undefined;
} else if (['allow-insecure'].includes(key)) { } else if (['allow-insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value); proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
} else if (['fast-open'].includes(key)) {
proxy.tfo = true;
} else if (['disable-sni', 'reduce-rtt'].includes(key)) { } else if (['disable-sni', 'reduce-rtt'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value); proxy[key] = /(TRUE)|1/i.test(value);
} else if (key === 'congestion-control') {
proxy['congestion-controller'] = value;
delete proxy[key];
} else { } else {
proxy[key] = value; proxy[key] = value;
} }
@ -929,6 +1140,7 @@ function Clash_All() {
const proxy = JSON.parse(line); const proxy = JSON.parse(line);
if ( if (
![ ![
'anytls',
'mieru', 'mieru',
'juicity', 'juicity',
'ss', 'ss',
@ -1431,6 +1643,7 @@ function isIP(ip) {
export default [ export default [
URI_PROXY(), URI_PROXY(),
URI_SOCKS(),
URI_SS(), URI_SS(),
URI_SSR(), URI_SSR(),
URI_VMess(), URI_VMess(),
@ -1440,6 +1653,7 @@ export default [
URI_Hysteria(), URI_Hysteria(),
URI_Hysteria2(), URI_Hysteria2(),
URI_Trojan(), URI_Trojan(),
URI_AnyTLS(),
Clash_All(), Clash_All(),
Surge_Direct(), Surge_Direct(),
Surge_SSH(), Surge_SSH(),

View File

@ -39,12 +39,12 @@ start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2
return proxy; return proxy;
} }
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{
proxy.type = "ssr"; proxy.type = "ssr";
// handle ssr obfs // handle ssr obfs
proxy.obfs = obfs.type; proxy.obfs = obfs.type;
} }
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle ss obfs // handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@ -54,31 +54,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* { vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
handleTransport(); handleTransport();
} }
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
proxy.type = "vless"; proxy.type = "vless";
handleTransport(); handleTransport();
} }
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* { hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
} }
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
} }
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@ -169,19 +169,31 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; } tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@ -37,12 +37,12 @@ start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2
return proxy; return proxy;
} }
shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/others)*{ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/ssr_protocol_param/obfs_ssr/obfs_ssr_param/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)*{
proxy.type = "ssr"; proxy.type = "ssr";
// handle ssr obfs // handle ssr obfs
proxy.obfs = obfs.type; proxy.obfs = obfs.type;
} }
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* { shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/udp_port/shadow_tls_version/shadow_tls_sni/shadow_tls_password/ip_mode/block_quic/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle ss obfs // handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@ -52,31 +52,31 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path); $set(proxy, "plugin-opts.path", obfs.path);
} }
} }
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/others)* { vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0; proxy.alterId = proxy.alterId || 0;
handleTransport(); handleTransport();
} }
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
proxy.type = "vless"; proxy.type = "vless";
handleTransport(); handleTransport();
} }
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleTransport(); handleTransport();
} }
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/others)* { hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
} }
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
} }
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* { http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
} }
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/others)* { socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
} }
@ -167,19 +167,31 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); } vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; } tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; } tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); } tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; } ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@ -49,7 +49,7 @@ trojan = "trojan" equals address
} }
shadowsocks = "shadowsocks" equals address shadowsocks = "shadowsocks" equals address
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* { (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/others)* {
if (proxy.protocol || proxy.type === "ssr") { if (proxy.protocol || proxy.type === "ssr") {
proxy.type = "ssr"; proxy.type = "ssr";
if (!proxy.protocol) { if (!proxy.protocol) {
@ -86,10 +86,10 @@ vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* { (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 === false) {
proxy.alterId = 0; proxy.alterId = 1;
} else { } else {
proxy.alterId = proxy.alterId || 0; proxy.alterId = 0;
} }
handleObfs(); handleObfs();
} }
@ -145,18 +145,20 @@ port = digits:[0-9]+ {
} }
} }
username = comma "username" equals username:[^=,]+ { proxy.username = username.join("").trim(); } username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
password = comma "password" equals password:[^=,]+ { proxy.password = password.join("").trim(); } password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); } uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
method = comma "method" equals cipher:cipher { method = comma "method" equals cipher:cipher {
proxy.cipher = cipher; proxy.cipher = cipher;
}; };
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"); cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
aead = comma "aead" equals flag:bool { proxy.aead = flag; } aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); } udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); }
udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }

View File

@ -47,7 +47,7 @@ trojan = "trojan" equals address
} }
shadowsocks = "shadowsocks" equals address shadowsocks = "shadowsocks" equals address
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/server_check_url/others)* { (password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp_new/fast_open/tag/server_check_url/others)* {
if (proxy.protocol || proxy.type === "ssr") { if (proxy.protocol || proxy.type === "ssr") {
proxy.type = "ssr"; proxy.type = "ssr";
if (!proxy.protocol) { if (!proxy.protocol) {
@ -84,10 +84,10 @@ vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* { (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 === false) {
proxy.alterId = 0; proxy.alterId = 1;
} else { } else {
proxy.alterId = proxy.alterId || 0; proxy.alterId = 0;
} }
handleObfs(); handleObfs();
} }
@ -143,18 +143,20 @@ port = digits:[0-9]+ {
} }
} }
username = comma "username" equals username:[^=,]+ { proxy.username = username.join("").trim(); } username = comma "username" equals username:[^,]+ { proxy.username = username.join("").trim(); }
password = comma "password" equals password:[^=,]+ { proxy.password = password.join("").trim(); } password = comma "password" equals password:[^,]+ { proxy.password = password.join("").trim(); }
uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim(); } uuid = comma "password" equals uuid:[^,]+ { proxy.uuid = uuid.join("").trim(); }
method = comma "method" equals cipher:cipher { method = comma "method" equals cipher:cipher {
proxy.cipher = cipher; proxy.cipher = cipher;
}; };
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"); cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"none"/"rc2-cfb"/"rc4-md5-6"/"rc4-md5"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
aead = comma "aead" equals flag:bool { proxy.aead = flag; } aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; } udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); } udp_over_tcp = comma "udp-over-tcp" equals flag:bool { throw new Error("UDP over TCP is not supported"); }
udp_over_tcp_new = comma "udp-over-tcp" equals param:$[^=,]+ { if (param === "sp.v1") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 1; } else if (param === "sp.v2") { proxy["udp-over-tcp"] = true; proxy["udp-over-tcp-version"] = 2; } else if (param === "true") { proxy["_ssr_python_uot"] = true; } else { throw new Error("Invalid value for udp-over-tcp"); } }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; } fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; } over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }

View File

@ -41,7 +41,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@ -52,36 +52,37 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
} }
handleShadowTLS(); handleShadowTLS();
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
if (proxy.aead) { if (proxy.aead) {
proxy.alterId = 0; proxy.alterId = 0;
} else { } else {
proxy.alterId = proxy.alterId || 0; proxy.alterId = 1;
} }
handleWebsocket(); handleWebsocket();
handleShadowTLS(); handleShadowTLS();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
handleShadowTLS(); handleShadowTLS();
} }
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); handleShadowTLS();
} }
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
handleShadowTLS(); handleShadowTLS();
} }
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ssh"; proxy.type = "ssh";
handleShadowTLS(); handleShadowTLS();
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@ -104,20 +105,20 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS(); handleShadowTLS();
} }
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* { hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/fast_open/tfo/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2"; proxy.type = "hysteria2";
handleShadowTLS(); handleShadowTLS();
} }
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
handleShadowTLS(); handleShadowTLS();
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); handleShadowTLS();
} }
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/block_quic/others)* { direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct"; proxy.type = "direct";
} }
@ -246,7 +247,7 @@ block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); } udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); } shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@ -39,7 +39,7 @@ start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v
return proxy; return proxy;
} }
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* { shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss"; proxy.type = "ss";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@ -50,36 +50,37 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
} }
handleShadowTLS(); handleShadowTLS();
} }
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "vmess"; proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none"; proxy.cipher = proxy.cipher || "none";
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
if (proxy.aead) { if (proxy.aead) {
proxy.alterId = 0; proxy.alterId = 0;
} else { } else {
proxy.alterId = proxy.alterId || 0; proxy.alterId = 1;
} }
handleWebsocket(); handleWebsocket();
handleShadowTLS(); handleShadowTLS();
} }
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "trojan"; proxy.type = "trojan";
handleWebsocket(); handleWebsocket();
handleShadowTLS(); handleShadowTLS();
} }
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); handleShadowTLS();
} }
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http"; proxy.type = "http";
handleShadowTLS(); handleShadowTLS();
} }
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ssh"; proxy.type = "ssh";
handleShadowTLS(); handleShadowTLS();
} }
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/reuse/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "snell"; proxy.type = "snell";
// handle obfs // handle obfs
if (obfs.type == "http" || obfs.type === "tls") { if (obfs.type == "http" || obfs.type === "tls") {
@ -106,16 +107,16 @@ hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying
proxy.type = "hysteria2"; proxy.type = "hysteria2";
handleShadowTLS(); handleShadowTLS();
} }
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
handleShadowTLS(); handleShadowTLS();
} }
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5"; proxy.type = "socks5";
proxy.tls = true; proxy.tls = true;
handleShadowTLS(); handleShadowTLS();
} }
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/block_quic/others)* { direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct"; proxy.type = "direct";
} }
address = comma server:server comma port:port { address = comma server:server comma port:port {
@ -243,7 +244,7 @@ block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); } udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); } shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); } shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); } token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); } alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); } uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@ -80,6 +80,9 @@ port = digits:[0-9]+ {
} }
params = "?" head:param tail:("&"@param)* { params = "?" head:param tail:("&"@param)* {
for (const [key, value] of Object.entries(params)) {
params[key] = decodeURIComponent(value);
}
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"]; proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp; proxy['client-fingerprint'] = params.fp;
@ -115,6 +118,27 @@ params = "?" head:param tail:("&"@param)* {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true); $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
} }
} }
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
if (Object.keys(opts).length > 0) {
$set(proxy, params.security+"-opts", opts);
}
}
} }
proxy.udp = toBool(params["udp"]); proxy.udp = toBool(params["udp"]);

View File

@ -78,6 +78,9 @@ port = digits:[0-9]+ {
} }
params = "?" head:param tail:("&"@param)* { params = "?" head:param tail:("&"@param)* {
for (const [key, value] of Object.entries(params)) {
params[key] = decodeURIComponent(value);
}
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]); proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"]; proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp; proxy['client-fingerprint'] = params.fp;
@ -113,6 +116,27 @@ params = "?" head:param tail:("&"@param)* {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true); $set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
} }
} }
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
if (Object.keys(opts).length > 0) {
$set(proxy, params.security+"-opts", opts);
}
}
} }
proxy.udp = toBool(params["udp"]); proxy.udp = toBool(params["udp"]);

View File

@ -1,5 +1,6 @@
import { safeLoad } from '@/utils/yaml'; import { safeLoad } from '@/utils/yaml';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import $ from '@/core/app';
function HTML() { function HTML() {
const name = 'HTML'; const name = 'HTML';
@ -15,6 +16,7 @@ function Base64Encoded() {
const keys = [ const keys = [
'dm1lc3M', // vmess 'dm1lc3M', // vmess
'c3NyOi8v', // ssr:// 'c3NyOi8v', // ssr://
'c29ja3M6Ly', // socks://
'dHJvamFu', // trojan 'dHJvamFu', // trojan
'c3M6Ly', // ss:/ 'c3M6Ly', // ss:/
'c3NkOi8v', // ssd:// 'c3NkOi8v', // ssd://
@ -35,8 +37,35 @@ function Base64Encoded() {
); );
}; };
const parse = function (raw) { const parse = function (raw) {
raw = Base64.decode(raw); const decoded = Base64.decode(raw);
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
$.error(
`Base64 Pre-processor error: decoded line does not start with protocol`,
);
return raw; return raw;
}
return decoded;
};
return { name, test, parse };
}
function fallbackBase64Encoded() {
const name = 'Fallback Base64 Pre-processor';
const test = function (raw) {
return true;
};
const parse = function (raw) {
const decoded = Base64.decode(raw);
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
$.error(
`Fallback Base64 Pre-processor error: decoded line does not start with protocol`,
);
return raw;
}
return decoded;
}; };
return { name, test, parse }; return { name, test, parse };
} }
@ -48,43 +77,50 @@ function Clash() {
const content = safeLoad(raw); const content = safeLoad(raw);
return content.proxies && Array.isArray(content.proxies); return content.proxies && Array.isArray(content.proxies);
}; };
const parse = function (raw) { const parse = function (raw, includeProxies) {
// Clash YAML format // Clash YAML format
// 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity // 防止 VLESS节点 reality-opts 选项中的 short-id 被解析成 Infinity
// 匹配 short-id 冒号后面的值(包含空格和引号) // 匹配 short-id 冒号后面的值(包含空格和引号)
const afterReplace = raw.replace( const afterReplace = raw.replace(
/short-id:([ ]*[^,\n}]*)/g, /short-id:([ \t]*[^#\n,}]*)/g,
(matched, value) => { (matched, value) => {
const afterTrim = value.trim(); const afterTrim = value.trim();
// 为空 // 为空
if (!afterTrim || afterTrim === '') { if (!afterTrim || afterTrim === '') {
return 'short-id: ""' return 'short-id: ""';
} }
// 是否被引号包裹 // 是否被引号包裹
if (/^(['"]).*\1$/.test(afterTrim)) { if (/^(['"]).*\1$/.test(afterTrim)) {
return `short-id: ${afterTrim}`; return `short-id: ${afterTrim}`;
} else if (['null'].includes(afterTrim)) {
return `short-id: ${afterTrim}`;
} else { } else {
return `short-id: "${afterTrim}"` return `short-id: "${afterTrim}"`;
}
} }
},
); );
const { const {
proxies, proxies,
'global-client-fingerprint': globalClientFingerprint, 'global-client-fingerprint': globalClientFingerprint,
} = safeLoad(afterReplace); } = safeLoad(afterReplace);
return proxies return (
(includeProxies ? 'proxies:\n' : '') +
proxies
.map((p) => { .map((p) => {
// https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26 // https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml#L73C1-L73C26
if (globalClientFingerprint && !p['client-fingerprint']) { if (globalClientFingerprint && !p['client-fingerprint']) {
p['client-fingerprint'] = globalClientFingerprint; p['client-fingerprint'] = globalClientFingerprint;
} }
return JSON.stringify(p); return `${includeProxies ? ' - ' : ''}${JSON.stringify(
p,
)}\n`;
}) })
.join('\n'); .join('')
);
}; };
return { name, test, parse }; return { name, test, parse };
} }
@ -147,4 +183,11 @@ function FullConfig() {
return { name, test, parse }; return { name, test, parse };
} }
export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()]; export default [
HTML(),
Clash(),
Base64Encoded(),
SSD(),
FullConfig(),
fallbackBase64Encoded(),
];

View File

@ -10,6 +10,7 @@ import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants'; import { SETTINGS_KEY } from '@/constants';
import YAML from '@/utils/yaml';
import env from '@/utils/env'; import env from '@/utils/env';
import { import {
@ -19,8 +20,49 @@ import {
validCheck, validCheck,
flowTransfer, flowTransfer,
getRmainingDays, getRmainingDays,
normalizeFlowHeader,
} from '@/utils/flow'; } from '@/utils/flow';
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
function trimWrap(str) {
if (str.startsWith('<') && str.endsWith('>')) {
return str.slice(1, -1);
}
return str;
}
function deepMerge(target, _other) {
const other = typeof _other === 'string' ? JSON.parse(_other) : _other;
for (const key in other) {
if (isObject(other[key])) {
if (key.endsWith('!')) {
const k = trimWrap(key.slice(0, -1));
target[k] = other[key];
} else {
const k = trimWrap(key);
if (!target[k]) Object.assign(target, { [k]: {} });
deepMerge(target[k], other[k]);
}
} else if (Array.isArray(other[key])) {
if (key.startsWith('+')) {
const k = trimWrap(key.slice(1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...other[key], ...target[k]];
} else if (key.endsWith('+')) {
const k = trimWrap(key.slice(0, -1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...target[k], ...other[key]];
} else {
const k = trimWrap(key);
Object.assign(target, { [k]: other[key] });
}
} else {
Object.assign(target, { [key]: other[key] });
}
}
return target;
}
/** /**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows: The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{ {
@ -242,7 +284,15 @@ function SortOperator(order = 'asc') {
} }
// sort by regex // sort by regex
function RegexSortOperator(expressions) { function RegexSortOperator(input) {
const order = input.order || 'asc';
let expressions = input.expressions;
if (Array.isArray(input)) {
expressions = input;
}
if (!Array.isArray(expressions)) {
expressions = [];
}
return { return {
name: 'Regex Sort Operator', name: 'Regex Sort Operator',
func: (proxies) => { func: (proxies) => {
@ -253,8 +303,13 @@ function RegexSortOperator(expressions) {
if (oA && !oB) return -1; if (oA && !oB) return -1;
if (oB && !oA) return 1; if (oB && !oA) return 1;
if (oA && oB) return oA < oB ? -1 : 1; if (oA && oB) return oA < oB ? -1 : 1;
if ((!oA && !oB) || (oA && oB && oA === oB)) if (order === 'original') {
return a.name < b.name ? -1 : 1; // fallback to normal sort return 0;
} else if (order === 'desc') {
return a.name < b.name ? 1 : -1;
} else {
return a.name < b.name ? -1 : 1;
}
}); });
}, },
}; };
@ -321,6 +376,46 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
let output = proxies; let output = proxies;
if (output?.$file?.type === 'mihomoProfile') {
try {
let patch = YAML.safeLoad(script);
let config;
if (output?.$content) {
try {
config = YAML.safeLoad(output?.$content);
} catch (e) {
$.error(e.message ?? e);
}
}
// if (typeof patch !== 'object') patch = {};
if (typeof patch !== 'object')
throw new Error('patch is not an object');
output.$content = ProxyUtils.yaml.safeDump(
deepMerge(
config ||
(output?.$file?.sourceType === 'none'
? {}
: {
proxies: await produceArtifact({
type:
output?.$file?.sourceType ||
'collection',
name: output?.$file?.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true,
},
}),
}),
patch,
),
);
return output;
} catch (e) {
// console.log(e);
}
}
await (async function () { await (async function () {
const operator = createDynamicFunction( const operator = createDynamicFunction(
'operator', 'operator',
@ -339,9 +434,34 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
'operator', 'operator',
`async function operator(input = []) { `async function operator(input = []) {
if (input && (input.$files || input.$content)) { if (input && (input.$files || input.$content)) {
let { $content, $files, $options } = input let { $content, $files, $options, $file } = input
if($file.type === 'mihomoProfile') {
${script} ${script}
return { $content, $files, $options } if(typeof main === 'function') {
let config;
if ($content) {
try {
config = ProxyUtils.yaml.safeLoad($content);
} catch (e) {
console.log(e.message ?? e);
}
}
$content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {
proxies: await produceArtifact({
type: $file.sourceType || 'collection',
name: $file.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true
}
}),
})))
}
} else {
${script}
}
return { $content, $files, $options, $file }
} else { } else {
let proxies = input let proxies = input
let list = [] let list = []
@ -742,7 +862,12 @@ function UselessFilter() {
} }
// filter by regions // filter by regions
function RegionFilter(regions) { function RegionFilter(input) {
let regions = input?.value || input;
if (!Array.isArray(regions)) {
regions = [];
}
const keep = input?.keep ?? true;
const REGION_MAP = { const REGION_MAP = {
HK: '🇭🇰', HK: '🇭🇰',
TW: '🇹🇼', TW: '🇹🇼',
@ -759,7 +884,8 @@ function RegionFilter(regions) {
// this would be high memory usage // this would be high memory usage
return proxies.map((proxy) => { return proxies.map((proxy) => {
const flag = getFlag(proxy.name); const flag = getFlag(proxy.name);
return regions.some((r) => REGION_MAP[r] === flag); const selected = regions.some((r) => REGION_MAP[r] === flag);
return keep ? selected : !selected;
}); });
}, },
}; };
@ -791,11 +917,19 @@ function buildRegex(str, ...options) {
} }
// filter by proxy types // filter by proxy types
function TypeFilter(types) { function TypeFilter(input) {
let types = input?.value || input;
if (!Array.isArray(types)) {
types = [];
}
const keep = input?.keep ?? true;
return { return {
name: 'Type Filter', name: 'Type Filter',
func: (proxies) => { func: (proxies) => {
return proxies.map((proxy) => types.some((t) => proxy.type === t)); return proxies.map((proxy) => {
const selected = types.some((t) => proxy.type === t);
return keep ? selected : !selected;
});
}, },
}; };
} }
@ -995,6 +1129,7 @@ function createDynamicFunction(name, script, $arguments, $options) {
flowTransfer, flowTransfer,
validCheck, validCheck,
getRmainingDays, getRmainingDays,
normalizeFlowHeader,
}; };
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(
@ -1006,6 +1141,10 @@ function createDynamicFunction(name, script, $arguments, $options) {
'$httpClient', '$httpClient',
'$notification', '$notification',
'ProxyUtils', 'ProxyUtils',
'yaml',
'Buffer',
'b64d',
'b64e',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils', 'flowUtils',
'produceArtifact', 'produceArtifact',
@ -1023,6 +1162,10 @@ function createDynamicFunction(name, script, $arguments, $options) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
$notification, $notification,
ProxyUtils, ProxyUtils,
ProxyUtils.yaml,
ProxyUtils.Buffer,
ProxyUtils.Base64.decode,
ProxyUtils.Base64.encode,
scriptResourceCache, scriptResourceCache,
flowUtils, flowUtils,
produceArtifact, produceArtifact,
@ -1035,6 +1178,10 @@ function createDynamicFunction(name, script, $arguments, $options) {
'$substore', '$substore',
'lodash', 'lodash',
'ProxyUtils', 'ProxyUtils',
'yaml',
'Buffer',
'b64d',
'b64e',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils', 'flowUtils',
'produceArtifact', 'produceArtifact',
@ -1046,6 +1193,10 @@ function createDynamicFunction(name, script, $arguments, $options) {
$, $,
lodash, lodash,
ProxyUtils, ProxyUtils,
ProxyUtils.yaml,
ProxyUtils.Buffer,
ProxyUtils.Base64.decode,
ProxyUtils.Base64.encode,
scriptResourceCache, scriptResourceCache,
flowUtils, flowUtils,
produceArtifact, produceArtifact,

View File

@ -1,4 +1,5 @@
import { isPresent } from '@/core/proxy-utils/producers/utils'; import { isPresent } from '@/core/proxy-utils/producers/utils';
import $ from '@/core/app';
export default function Clash_Producer() { export default function Clash_Producer() {
const type = 'ALL'; const type = 'ALL';
@ -46,6 +47,11 @@ export default function Clash_Producer() {
proxy['reality-opts'])) proxy['reality-opts']))
) { ) {
return false; return false;
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
$.error(
`Clash 不支持前置代理字段. 已过滤节点 ${proxy.name}`,
);
return false;
} }
return true; return true;
}) })
@ -81,6 +87,8 @@ export default function Clash_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') { } else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) { if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
@ -139,6 +147,7 @@ export default function Clash_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
@ -149,11 +158,6 @@ export default function Clash_Producer() {
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls; delete proxy.tls;
} }

View File

@ -10,6 +10,47 @@ export default function ClashMeta_Producer() {
return false; return false;
} else if (['juicity'].includes(proxy.type)) { } else if (['juicity'].includes(proxy.type)) {
return false; return false;
} else if (
['ss'].includes(proxy.type) &&
![
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-ccm',
'aes-192-ccm',
'aes-256-ccm',
'aes-128-gcm-siv',
'aes-256-gcm-siv',
'chacha20-ietf',
'chacha20',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'chacha8-ietf-poly1305',
'xchacha8-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305',
'lea-128-gcm',
'lea-192-gcm',
'lea-256-gcm',
'rabbit128-poly1305',
'aegis-128l',
'aegis-256',
'aez-384',
'deoxys-ii-256-128',
'rc4-md5',
'none',
].includes(proxy.cipher)
) {
// https://wiki.metacubex.one/config/proxies/ss/#cipher
return false;
} }
return true; return true;
}) })
@ -32,9 +73,10 @@ export default function ClashMeta_Producer() {
isPresent(proxy, 'cipher') && isPresent(proxy, 'cipher') &&
![ ![
'auto', 'auto',
'none',
'zero',
'aes-128-gcm', 'aes-128-gcm',
'chacha20-poly1305', 'chacha20-poly1305',
'none',
].includes(proxy.cipher) ].includes(proxy.cipher)
) { ) {
proxy.cipher = 'auto'; proxy.cipher = 'auto';
@ -86,6 +128,8 @@ export default function ClashMeta_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') { } else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) { if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
@ -102,6 +146,9 @@ export default function ClashMeta_Producer() {
password: proxy['shadow-tls-password'], password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'], version: proxy['shadow-tls-version'],
}; };
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
} }
} }
@ -157,6 +204,7 @@ export default function ClashMeta_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
@ -180,7 +228,7 @@ export default function ClashMeta_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
if (type !== 'internal') { if (type !== 'internal' || opts['delete-underscore-fields']) {
for (const key in proxy) { for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) { if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key]; delete proxy[key];

View File

@ -1,10 +1,12 @@
import { isPresent } from './utils';
export default function Egern_Producer() { export default function Egern_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies, type, opts = {}) => { const produce = (proxies, type) => {
// https://egernapp.com/zh-CN/docs/configuration/proxies // https://egernapp.com/zh-CN/docs/configuration/proxies
const list = proxies const list = proxies
.filter((proxy) => { .filter((proxy) => {
if (opts['include-unsupported-proxy']) return true; // if (opts['include-unsupported-proxy']) return true;
if ( if (
![ ![
'http', 'http',
@ -14,6 +16,7 @@ export default function Egern_Producer() {
'hysteria2', 'hysteria2',
'vless', 'vless',
'vmess', 'vmess',
'tuic',
].includes(proxy.type) || ].includes(proxy.type) ||
(proxy.type === 'ss' && (proxy.type === 'ss' &&
((proxy.plugin === 'obfs' && ((proxy.plugin === 'obfs' &&
@ -47,17 +50,12 @@ export default function Egern_Producer() {
'salsa20', 'salsa20',
'chacha20', 'chacha20',
'chacha20-ietf', 'chacha20-ietf',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher))) || ].includes(proxy.cipher))) ||
(proxy.type === 'vmess' && (proxy.type === 'vmess' &&
(![ !['http', 'ws', 'tcp'].includes(proxy.network) &&
'auto', proxy.network) ||
'aes-128-gcm',
'chacha20-poly1305',
'none',
'zero',
].includes(proxy.cipher) ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network))) ||
(proxy.type === 'trojan' && (proxy.type === 'trojan' &&
!['http', 'ws', 'tcp'].includes(proxy.network) && !['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) || proxy.network) ||
@ -65,16 +63,26 @@ export default function Egern_Producer() {
(typeof proxy.flow !== 'undefined' || (typeof proxy.flow !== 'undefined' ||
proxy['reality-opts'] || proxy['reality-opts'] ||
(!['http', 'ws', 'tcp'].includes(proxy.network) && (!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network))) proxy.network))) ||
(proxy.type === 'tuic' &&
proxy.token &&
proxy.token.length !== 0)
) { ) {
return false; return false;
} }
return true; return true;
}) })
.map((proxy) => { .map((proxy) => {
const original = { ...proxy };
if (proxy.tls && !proxy.sni) { if (proxy.tls && !proxy.sni) {
proxy.sni = proxy.server; proxy.sni = proxy.server;
} }
const prev_hop =
proxy.prev_hop ||
proxy['underlying-proxy'] ||
proxy['dialer-proxy'] ||
proxy.detour;
if (proxy.type === 'http') { if (proxy.type === 'http') {
proxy = { proxy = {
type: 'http', type: 'http',
@ -115,10 +123,10 @@ export default function Egern_Producer() {
proxy.udp || proxy.udp_relay || proxy.udp_relay, proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop, next_hop: proxy.next_hop,
}; };
if (proxy.plugin === 'obfs') { if (original.plugin === 'obfs') {
proxy.obfs = proxy['plugin-opts'].mode; proxy.obfs = original['plugin-opts'].mode;
proxy.obfs_host = proxy['plugin-opts'].host; proxy.obfs_host = original['plugin-opts'].host;
proxy.obfs_uri = proxy['plugin-opts'].path; proxy.obfs_uri = original['plugin-opts'].path;
} }
} else if (proxy.type === 'hysteria2') { } else if (proxy.type === 'hysteria2') {
proxy = { proxy = {
@ -133,11 +141,33 @@ export default function Egern_Producer() {
next_hop: proxy.next_hop, next_hop: proxy.next_hop,
sni: proxy.sni, sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'], skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
}; };
if (proxy['obfs-password'] && proxy.obfs == 'salamander') { if (
original['obfs-password'] &&
original.obfs == 'salamander'
) {
proxy.obfs = 'salamander'; proxy.obfs = 'salamander';
proxy.obfs_password = proxy['obfs-password']; proxy.obfs_password = original['obfs-password'];
} }
} else if (proxy.type === 'tuic') {
proxy = {
type: 'tuic',
name: proxy.name,
server: proxy.server,
port: proxy.port,
uuid: proxy.uuid,
password: proxy.password,
next_hop: proxy.next_hop,
sni: proxy.sni,
alpn: Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn || 'h3'],
skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
};
} else if (proxy.type === 'trojan') { } else if (proxy.type === 'trojan') {
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
proxy.websocket = { proxy.websocket = {
@ -160,6 +190,20 @@ export default function Egern_Producer() {
websocket: proxy.websocket, websocket: proxy.websocket,
}; };
} else if (proxy.type === 'vmess') { } else if (proxy.type === 'vmess') {
// Egern传输层支持 ws/wss/http1/http2/tls不配置则为 tcp
let security = proxy.cipher;
if (
security &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
].includes(security)
) {
security = 'auto';
}
if (proxy.network === 'ws') { if (proxy.network === 'ws') {
proxy.transport = { proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: { [proxy.tls ? 'wss' : 'ws']: {
@ -175,9 +219,11 @@ export default function Egern_Producer() {
}; };
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
proxy.transport = { proxy.transport = {
http: { http1: {
method: proxy['http-opts']?.method, method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path, path: Array.isArray(proxy['http-opts']?.path)
? proxy['http-opts']?.path[0]
: proxy['http-opts']?.path,
headers: { headers: {
Host: Array.isArray( Host: Array.isArray(
proxy['http-opts']?.headers?.Host, proxy['http-opts']?.headers?.Host,
@ -188,9 +234,29 @@ export default function Egern_Producer() {
skip_tls_verify: proxy['skip-cert-verify'], skip_tls_verify: proxy['skip-cert-verify'],
}, },
}; };
} else if (proxy.network === 'tcp' || !proxy.network) { } else if (proxy.network === 'h2') {
proxy.transport = { proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: { http2: {
method: proxy['h2-opts']?.method,
path: Array.isArray(proxy['h2-opts']?.path)
? proxy['h2-opts']?.path[0]
: proxy['h2-opts']?.path,
headers: {
Host: Array.isArray(
proxy['h2-opts']?.headers?.Host,
)
? proxy['h2-opts']?.headers?.Host[0]
: proxy['h2-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (
(proxy.network === 'tcp' || !proxy.network) &&
proxy.tls
) {
proxy.transport = {
tls: {
sni: proxy.tls ? proxy.sni : undefined, sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls skip_tls_verify: proxy.tls
? proxy['skip-cert-verify'] ? proxy['skip-cert-verify']
@ -204,7 +270,7 @@ export default function Egern_Producer() {
server: proxy.server, server: proxy.server,
port: proxy.port, port: proxy.port,
user_id: proxy.uuid, user_id: proxy.uuid,
security: proxy.cipher, security,
tfo: proxy.tfo || proxy['fast-open'], tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy, legacy: proxy.legacy,
udp_relay: udp_relay:
@ -232,7 +298,9 @@ export default function Egern_Producer() {
proxy.transport = { proxy.transport = {
http: { http: {
method: proxy['http-opts']?.method, method: proxy['http-opts']?.method,
path: proxy['http-opts']?.path, path: Array.isArray(proxy['http-opts']?.path)
? proxy['http-opts']?.path[0]
: proxy['http-opts']?.path,
headers: { headers: {
Host: Array.isArray( Host: Array.isArray(
proxy['http-opts']?.headers?.Host, proxy['http-opts']?.headers?.Host,
@ -270,6 +338,39 @@ export default function Egern_Producer() {
// skip_tls_verify: proxy['skip-cert-verify'], // skip_tls_verify: proxy['skip-cert-verify'],
}; };
} }
if (
[
'http',
'socks5',
'ss',
'trojan',
'vless',
'vmess',
].includes(original.type)
) {
if (isPresent(original, 'shadow-tls-password')) {
if (original['shadow-tls-version'] != 3)
throw new Error(
`shadow-tls version ${original['shadow-tls-version']} is not supported`,
);
proxy.shadow_tls = {
password: original['shadow-tls-password'],
sni: original['shadow-tls-sni'],
};
} else if (
['shadow-tls'].includes(original.plugin) &&
original['plugin-opts']
) {
if (original['plugin-opts'].version != 3)
throw new Error(
`shadow-tls version ${original['plugin-opts'].version} is not supported`,
);
proxy.shadow_tls = {
password: original['plugin-opts'].password,
sni: original['plugin-opts'].host,
};
}
}
delete proxy.subName; delete proxy.subName;
delete proxy.collectionName; delete proxy.collectionName;
@ -287,6 +388,7 @@ export default function Egern_Producer() {
[proxy.type]: { [proxy.type]: {
...proxy, ...proxy,
type: undefined, type: undefined,
prev_hop,
}, },
}; };
}); });

View File

@ -20,20 +20,37 @@ function JSON_Producer() {
} }
export default { export default {
qx: QX_Producer(),
QX: QX_Producer(), QX: QX_Producer(),
QuantumultX: QX_Producer(), QuantumultX: QX_Producer(),
surge: Surge_Producer(),
Surge: Surge_Producer(), Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(), SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(), Loon: Loon_Producer(),
Clash: Clash_Producer(), Clash: Clash_Producer(),
meta: ClashMeta_Producer(),
clashmeta: ClashMeta_Producer(),
'clash.meta': ClashMeta_Producer(),
'Clash.Meta': ClashMeta_Producer(),
ClashMeta: ClashMeta_Producer(), ClashMeta: ClashMeta_Producer(),
mihomo: ClashMeta_Producer(),
Mihomo: ClashMeta_Producer(),
uri: URI_Producer(),
URI: URI_Producer(), URI: URI_Producer(),
v2: V2Ray_Producer(),
v2ray: V2Ray_Producer(),
V2Ray: V2Ray_Producer(), V2Ray: V2Ray_Producer(),
json: JSON_Producer(),
JSON: JSON_Producer(), JSON: JSON_Producer(),
stash: Stash_Producer(),
Stash: Stash_Producer(), Stash: Stash_Producer(),
shadowrocket: Shadowrocket_Producer(),
Shadowrocket: Shadowrocket_Producer(), Shadowrocket: Shadowrocket_Producer(),
ShadowRocket: Shadowrocket_Producer(), ShadowRocket: Shadowrocket_Producer(),
surfboard: Surfboard_Producer(),
Surfboard: Surfboard_Producer(), Surfboard: Surfboard_Producer(),
singbox: singbox_Producer(),
'sing-box': singbox_Producer(), 'sing-box': singbox_Producer(),
egern: Egern_Producer(),
Egern: Egern_Producer(), Egern: Egern_Producer(),
}; };

View File

@ -3,19 +3,27 @@ const targetPlatform = 'Loon';
import { isPresent, Result } from './utils'; import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils'; import { isIPv4, isIPv6 } from '@/utils';
const ipVersions = {
dual: 'dual',
ipv4: 'v4-only',
ipv6: 'v6-only',
'ipv4-prefer': 'prefer-v4',
'ipv6-prefer': 'prefer-v6',
};
export default function Loon_Producer() { export default function Loon_Producer() {
const produce = (proxy, type, opts = {}) => { const produce = (proxy, type, opts = {}) => {
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
return shadowsocks(proxy, opts['include-unsupported-proxy']); return shadowsocks(proxy);
case 'ssr': case 'ssr':
return shadowsocksr(proxy); return shadowsocksr(proxy);
case 'trojan': case 'trojan':
return trojan(proxy); return trojan(proxy);
case 'vmess': case 'vmess':
return vmess(proxy); return vmess(proxy, opts['include-unsupported-proxy']);
case 'vless': case 'vless':
return vless(proxy); return vless(proxy, opts['include-unsupported-proxy']);
case 'http': case 'http':
return http(proxy); return http(proxy);
case 'socks5': case 'socks5':
@ -32,7 +40,7 @@ export default function Loon_Producer() {
return { produce }; return { produce };
} }
function shadowsocks(proxy, includeUnsupportedProxy) { function shadowsocks(proxy) {
const result = new Result(proxy); const result = new Result(proxy);
if ( if (
![ ![
@ -56,9 +64,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
'aes-256-gcm', 'aes-256-gcm',
'chacha20-ietf-poly1305', 'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305', 'xchacha20-ietf-poly1305',
...(includeUnsupportedProxy '2022-blake3-aes-128-gcm',
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm'] '2022-blake3-aes-256-gcm',
: []),
].includes(proxy.cipher) ].includes(proxy.cipher)
) { ) {
throw new Error(`cipher ${proxy.cipher} is not supported`); throw new Error(`cipher ${proxy.cipher} is not supported`);
@ -79,19 +86,68 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
`,obfs-uri=${proxy['plugin-opts'].path}`, `,obfs-uri=${proxy['plugin-opts'].path}`,
'plugin-opts.path', 'plugin-opts.path',
); );
} else { } else if (!['shadow-tls'].includes(proxy.plugin)) {
throw new Error(`plugin ${proxy.plugin} is not supported`); throw new Error(`plugin ${proxy.plugin} is not supported`);
} }
} }
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
@ -112,14 +168,63 @@ function shadowsocksr(proxy) {
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs'); result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param'); result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
@ -168,15 +273,26 @@ function trojan(proxy) {
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
function vmess(proxy) { function vmess(proxy) {
const isReality = !!proxy['reality-opts'];
const result = new Result(proxy); const result = new Result(proxy);
result.append( result.append(
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`, `${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
@ -224,6 +340,17 @@ function vmess(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
if (isReality) {
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,public-key="${proxy['reality-opts']['public-key']}"`,
'reality-opts.public-key',
);
result.appendIfPresent(
`,short-id=${proxy['reality-opts']['short-id']}`,
'reality-opts.short-id',
);
} else {
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent( result.appendIfPresent(
@ -234,10 +361,11 @@ function vmess(proxy) {
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256', 'tls-pubkey-sha256',
); );
}
// AEAD // AEAD
if (isPresent(proxy, 'aead')) { if (isPresent(proxy, 'aead')) {
result.append(`,alterId=0`); result.append(`,alterId=${proxy.aead ? 0 : 1}`);
} else { } else {
result.append(`,alterId=${proxy.alterId}`); result.append(`,alterId=${proxy.alterId}`);
} }
@ -245,17 +373,34 @@ function vmess(proxy) {
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
function vless(proxy) { function vless(proxy) {
if (proxy['reality-opts']) { let isXtls = false;
throw new Error(`VLESS REALITY is unsupported`); const isReality = !!proxy['reality-opts'];
if (typeof proxy.flow !== 'undefined') {
if (['xtls-rprx-vision'].includes(proxy.flow)) {
isXtls = true;
} else {
throw new Error(`VLESS flow(${proxy.flow}) is not supported`);
} }
}
const result = new Result(proxy); 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}"`,
@ -303,6 +448,20 @@ function vless(proxy) {
'skip-cert-verify', 'skip-cert-verify',
); );
if (isXtls) {
result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');
}
if (isReality) {
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,public-key="${proxy['reality-opts']['public-key']}"`,
'reality-opts.public-key',
);
result.appendIfPresent(
`,short-id=${proxy['reality-opts']['short-id']}`,
'reality-opts.short-id',
);
} else {
// sni // sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni'); result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent( result.appendIfPresent(
@ -313,14 +472,24 @@ function vless(proxy) {
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`, `,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256', 'tls-pubkey-sha256',
); );
}
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
@ -343,6 +512,16 @@ function http(proxy) {
// tfo // tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
function socks5(proxy) { function socks5(proxy) {
@ -366,10 +545,19 @@ function socks5(proxy) {
// tfo // tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
} }
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }
@ -435,6 +623,15 @@ function wireguard(proxy) {
presharedKey ?? '' presharedKey ?? ''
}}]`, }}]`,
); );
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
return result.toString(); return result.toString();
} }
@ -470,6 +667,13 @@ function hysteria2(proxy) {
// tfo // tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo'); result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp // udp
if (proxy.udp) { if (proxy.udp) {
result.append(`,udp=true`); result.append(`,udp=true`);
@ -482,6 +686,8 @@ function hysteria2(proxy) {
); );
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn'); result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString(); return result.toString();
} }

View File

@ -58,6 +58,8 @@ function shadowsocks(proxy) {
'aes-256-gcm', 'aes-256-gcm',
'chacha20-ietf-poly1305', 'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305', 'xchacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher) ].includes(proxy.cipher)
) { ) {
throw new Error(`cipher ${proxy.cipher} is not supported`); throw new Error(`cipher ${proxy.cipher} is not supported`);
@ -128,6 +130,20 @@ function shadowsocks(proxy) {
// udp // udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// udp over tcp
if (proxy['_ssr_python_uot']) {
append(`,udp-over-tcp=true`);
} else if (proxy['udp-over-tcp']) {
if (
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
) {
append(`,udp-over-tcp=sp.v1`);
} else if (proxy['udp-over-tcp-version'] === 2) {
append(`,udp-over-tcp=sp.v2`);
}
}
// server_check_url // server_check_url
result.appendIfPresent( result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`, `,server_check_url=${proxy['test-url']}`,
@ -389,6 +405,8 @@ function vless(proxy) {
else append(`,obfs=ws`); else append(`,obfs=ws`);
} else if (proxy.network === 'http') { } else if (proxy.network === 'http') {
append(`,obfs=http`); append(`,obfs=http`);
} else if (['tcp'].includes(proxy.network)) {
if (proxy.tls) append(`,obfs=over-tls`);
} else if (!['tcp'].includes(proxy.network)) { } else if (!['tcp'].includes(proxy.network)) {
throw new Error(`network ${proxy.network} is unsupported`); throw new Error(`network ${proxy.network} is unsupported`);
} }

View File

@ -1,6 +1,7 @@
import { isPresent } from '@/core/proxy-utils/producers/utils'; import { isPresent } from '@/core/proxy-utils/producers/utils';
import $ from '@/core/app';
export default function ShadowRocket_Producer() { export default function Shadowrocket_Producer() {
const type = 'ALL'; const type = 'ALL';
const produce = (proxies, type, opts = {}) => { const produce = (proxies, type, opts = {}) => {
const list = proxies const list = proxies
@ -32,9 +33,10 @@ export default function ShadowRocket_Producer() {
isPresent(proxy, 'cipher') && isPresent(proxy, 'cipher') &&
![ ![
'auto', 'auto',
'none',
'zero',
'aes-128-gcm', 'aes-128-gcm',
'chacha20-poly1305', 'chacha20-poly1305',
'none',
].includes(proxy.cipher) ].includes(proxy.cipher)
) { ) {
proxy.cipher = 'auto'; proxy.cipher = 'auto';
@ -102,11 +104,28 @@ export default function ShadowRocket_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') { } else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) { if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
delete proxy.sni; delete proxy.sni;
} }
} else if (proxy.type === 'ss') {
if (
isPresent(proxy, 'shadow-tls-password') &&
!isPresent(proxy, 'plugin')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
}
} }
if ( if (
@ -160,6 +179,7 @@ export default function ShadowRocket_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;

View File

@ -5,6 +5,10 @@ import { isIPv4, isIPv6 } from '@/utils';
const detourParser = (proxy, parsedProxy) => { const detourParser = (proxy, parsedProxy) => {
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour; parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
}; };
const networkParser = (proxy, parsedProxy) => {
if (['tcp', 'udp'].includes(proxy._network))
parsedProxy.network = proxy._network;
};
const tfoParser = (proxy, parsedProxy) => { const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false; parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true; if (proxy.tfo) parsedProxy.tcp_fast_open = true;
@ -27,6 +31,21 @@ const smuxParser = (smux, proxy) => {
if (smux['min-streams']) if (smux['min-streams'])
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10); proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
if (smux.padding) proxy.multiplex.padding = true; if (smux.padding) proxy.multiplex.padding = true;
if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) {
proxy.multiplex.brutal = {
enabled: true,
};
if (smux['brutal-opts']?.up)
proxy.multiplex.brutal.up_mbps = parseInt(
`${smux['brutal-opts']?.up}`,
10,
);
if (smux['brutal-opts']?.down)
proxy.multiplex.brutal.down_mbps = parseInt(
`${smux['brutal-opts']?.down}`,
10,
);
}
}; };
const wsParser = (proxy, parsedProxy) => { const wsParser = (proxy, parsedProxy) => {
@ -305,6 +324,7 @@ const socks5Parser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true; if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
return parsedProxy; return parsedProxy;
@ -354,8 +374,18 @@ const ssParser = (proxy = {}) => {
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port'; throw 'invalid port';
if (proxy.uot) parsedProxy.udp_over_tcp = true; if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true; if (proxy['udp-over-tcp']) {
parsedProxy.udp_over_tcp = {
enabled: true,
version:
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
? 1
: 2,
};
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
@ -470,7 +500,7 @@ const vmessParser = (proxy = {}) => {
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy); if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy); if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
@ -489,11 +519,12 @@ const vlessParser = (proxy = {}) => {
}; };
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port'; throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow; if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy); if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
@ -514,7 +545,7 @@ const trojanParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy); if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy); if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
@ -537,12 +568,13 @@ const hysteriaParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$'); const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
if (reg.test(`${proxy.up}`)) { // sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps
if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) {
parsedProxy.up = `${proxy.up}`; parsedProxy.up = `${proxy.up}`;
} else { } else {
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10); parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
} }
if (reg.test(`${proxy.down}`)) { if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) {
parsedProxy.down = `${proxy.down}`; parsedProxy.down = `${proxy.down}`;
} else { } else {
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10); parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
@ -562,13 +594,14 @@ const hysteriaParser = (proxy = {}) => {
parsedProxy.disable_mtu_discovery = true; parsedProxy.disable_mtu_discovery = true;
} }
} }
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => { const hysteria2Parser = (proxy = {}) => {
const parsedProxy = { const parsedProxy = {
tag: proxy.name, tag: proxy.name,
type: 'hysteria2', type: 'hysteria2',
@ -580,7 +613,6 @@ const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
}; };
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535) if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port'; throw 'invalid port';
if (includeUnsupportedProxy) {
if (proxy['hop-interval']) if (proxy['hop-interval'])
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval']) parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
? `${proxy['hop-interval']}s` ? `${proxy['hop-interval']}s`
@ -589,13 +621,13 @@ const hysteria2Parser = (proxy = {}, includeUnsupportedProxy) => {
parsedProxy.server_ports = proxy['ports'] parsedProxy.server_ports = proxy['ports']
.split(/\s*,\s*/) .split(/\s*,\s*/)
.map((p) => p.replace(/\s*-\s*/g, ':')); .map((p) => p.replace(/\s*-\s*/g, ':'));
}
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10); if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10); if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander'; if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password']) if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password']; parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs; if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
@ -626,12 +658,30 @@ const tuic5Parser = (proxy = {}) => {
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true; if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval']) if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`; parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy); tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
return parsedProxy; return parsedProxy;
}; };
const anytlsParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'anytls',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (/^\d+$/.test(proxy['idle-session-check-interval']))
parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
if (/^\d+$/.test(proxy['idle-session-timeout']))
parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => { const wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6'] const local_address = ['ip', 'ipv6']
@ -683,6 +733,7 @@ const wireguardParser = (proxy = {}) => {
parsedProxy.peers.push(peer); parsedProxy.peers.push(peer);
} }
} }
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy); detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy); smuxParser(proxy.smux, parsedProxy);
@ -819,6 +870,9 @@ export default function singbox_Producer() {
case 'wireguard': case 'wireguard':
list.push(wireguardParser(proxy)); list.push(wireguardParser(proxy));
break; break;
case 'anytls':
list.push(anytlsParser(proxy));
break;
default: default:
throw new Error( throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`, `Platform sing-box does not support proxy type: ${proxy.type}`,

View File

@ -1,4 +1,5 @@
import { isPresent } from '@/core/proxy-utils/producers/utils'; import { isPresent } from '@/core/proxy-utils/producers/utils';
import $ from '@/core/app';
export default function Stash_Producer() { export default function Stash_Producer() {
const type = 'ALL'; const type = 'ALL';
@ -39,17 +40,20 @@ export default function Stash_Producer() {
'xchacha20', 'xchacha20',
'chacha20-ietf-poly1305', 'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305', 'xchacha20-ietf-poly1305',
...(opts['include-unsupported-proxy']
? [
'2022-blake3-aes-128-gcm', '2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm', '2022-blake3-aes-256-gcm',
]
: []),
].includes(proxy.cipher)) || ].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') || (proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' && proxy['reality-opts']) (proxy.type === 'vless' &&
proxy['reality-opts'] &&
!['xtls-rprx-vision'].includes(proxy.flow))
) { ) {
return false; return false;
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
$.error(
`Stash 暂不支持前置代理字段. 已过滤节点 ${proxy.name}. 请使用 代理的转发链 https://stash.wiki/proxy-protocols/proxy-groups#relay`,
);
return false;
} }
return true; return true;
}) })
@ -187,6 +191,8 @@ export default function Stash_Producer() {
proxy['preshared-key'] = proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key']; proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key']; proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') { } else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) { if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni; proxy.servername = proxy.sni;
@ -245,6 +251,7 @@ export default function Stash_Producer() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'juicity', 'juicity',
'anytls',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
delete proxy.tls; delete proxy.tls;
@ -254,11 +261,6 @@ export default function Stash_Producer() {
} }
delete proxy['tls-fingerprint']; delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') { if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls; delete proxy.tls;
} }

View File

@ -53,7 +53,7 @@ export default function Surge_Producer() {
return { produce }; return { produce };
} }
function shadowsocks(proxy, includeUnsupportedProxy) { function shadowsocks(proxy) {
const result = new Result(proxy); const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
if (!proxy.cipher) { if (!proxy.cipher) {
@ -87,9 +87,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
'chacha20', 'chacha20',
'chacha20-ietf', 'chacha20-ietf',
'none', 'none',
...(includeUnsupportedProxy '2022-blake3-aes-128-gcm',
? ['2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm'] '2022-blake3-aes-256-gcm',
: []),
].includes(proxy.cipher) ].includes(proxy.cipher)
) { ) {
throw new Error(`cipher ${proxy.cipher} is not supported`); throw new Error(`cipher ${proxy.cipher} is not supported`);
@ -127,8 +126,6 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
// udp // udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp'); result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
// test-url // test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url'); result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
@ -160,6 +157,8 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`, `,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni', 'shadow-tls-sni',
); );
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) { } else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password; const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host; const host = proxy['plugin-opts'].host;
@ -177,6 +176,11 @@ function shadowsocks(proxy, includeUnsupportedProxy) {
} }
result.append(`,shadow-tls-version=${version}`); result.append(`,shadow-tls-version=${version}`);
} }
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
} }
} }
@ -429,6 +433,9 @@ function ssh(proxy) {
return result.toString(); return result.toString();
} }
function http(proxy) { function http(proxy) {
if (proxy.headers && Object.keys(proxy.headers).length > 0) {
throw new Error(`headers is unsupported`);
}
const result = new Result(proxy); const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http'; const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`); result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);

View File

@ -141,9 +141,18 @@ function mihomo(proxy, type, opts) {
dns: { dns: {
enable: true, enable: true,
ipv6, ipv6,
nameserver: [ 'default-nameserver': opts?.defaultNameserver ||
'https://223.6.6.6/dns-query', proxy._defaultNameserver || [
'https://120.53.53.53/dns-query', '180.76.76.76',
'52.80.52.52',
'119.28.28.28',
'223.6.6.6',
],
nameserver: opts?.nameserver ||
proxy._nameserver || [
'https://doh.pub/dns-query',
'https://dns.alidns.com/dns-query',
'https://doh-pure.onedns.net/dns-query',
], ],
}, },
proxies: [ proxies: [

View File

@ -12,7 +12,7 @@ export default function URI_Producer() {
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) { if (proxy[key] == null) {
delete proxy[key]; delete proxy[key];
} }
} }
@ -23,10 +23,21 @@ export default function URI_Producer() {
) { ) {
delete proxy.tls; delete proxy.tls;
} }
if (proxy.server && isIPv6(proxy.server)) { if (
!['vmess'].includes(proxy.type) &&
proxy.server &&
isIPv6(proxy.server)
) {
proxy.server = `[${proxy.server}]`; proxy.server = `[${proxy.server}]`;
} }
switch (proxy.type) { switch (proxy.type) {
case 'socks5':
result = `socks://${encodeURIComponent(
Base64.encode(
`${proxy.username ?? ''}:${proxy.password ?? ''}`,
),
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
break;
case 'ss': case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`; const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${ result = `ss://${
@ -54,6 +65,11 @@ export default function URI_Producer() {
}${opts.tls ? ';tls' : ''}`, }${opts.tls ? ';tls' : ''}`,
); );
break; break;
case 'shadow-tls':
result += encodeURIComponent(
`shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
);
break;
default: default:
throw new Error( throw new Error(
`Unsupported plugin option: ${proxy.plugin}`, `Unsupported plugin option: ${proxy.plugin}`,
@ -103,12 +119,17 @@ export default function URI_Producer() {
v: '2', 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, aid: `${proxy.alterId || 0}`,
aid: proxy.alterId || 0, scy: proxy.cipher,
net, net,
type,
tls: proxy.tls ? 'tls' : '', tls: proxy.tls ? 'tls' : '',
alpn: Array.isArray(proxy.alpn)
? proxy.alpn.join(',')
: proxy.alpn,
fp: proxy['client-fingerprint'],
}; };
if (proxy.tls && proxy.sni) { if (proxy.tls && proxy.sni) {
result.sni = proxy.sni; result.sni = proxy.sni;
@ -119,16 +140,7 @@ export default function URI_Producer() {
proxy[`${proxy.network}-opts`]?.path; proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost = let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host; 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)) { if (['grpc'].includes(proxy.network)) {
result.path = result.path =
proxy[`${proxy.network}-opts`]?.[ proxy[`${proxy.network}-opts`]?.[
@ -140,6 +152,31 @@ export default function URI_Producer() {
'gun'; 'gun';
result.host = result.host =
proxy[`${proxy.network}-opts`]?.['_grpc-authority']; proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
} else if (['kcp', 'quic'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-type`
] || 'none';
result.host =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-host`
];
result.path =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-path`
];
} else {
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
} }
} }
result = 'vmess://' + Base64.encode(JSON.stringify(result)); result = 'vmess://' + Base64.encode(JSON.stringify(result));
@ -149,6 +186,7 @@ export default function URI_Producer() {
const isReality = proxy['reality-opts']; const isReality = proxy['reality-opts'];
let sid = ''; let sid = '';
let pbk = ''; let pbk = '';
let spx = '';
if (isReality) { if (isReality) {
security = 'reality'; security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key']; const publicKey = proxy['reality-opts']?.['public-key'];
@ -159,6 +197,10 @@ export default function URI_Producer() {
if (shortId) { if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`; sid = `&sid=${encodeURIComponent(shortId)}`;
} }
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
spx = `&spx=${encodeURIComponent(spiderX)}`;
}
} else if (proxy.tls) { } else if (proxy.tls) {
security = 'tls'; security = 'tls';
} }
@ -188,6 +230,14 @@ export default function URI_Producer() {
if (proxy.flow) { if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`; flow = `&flow=${encodeURIComponent(proxy.flow)}`;
} }
let extra = '';
if (proxy._extra) {
extra = `&extra=${encodeURIComponent(proxy._extra)}`;
}
let mode = '';
if (proxy._mode) {
mode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
let vlessType = proxy.network; let vlessType = proxy.network;
if ( if (
proxy.network === 'ws' && proxy.network === 'ws' &&
@ -254,7 +304,7 @@ export default function URI_Producer() {
proxy.port proxy.port
}?security=${encodeURIComponent( }?security=${encodeURIComponent(
security, security,
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent( )}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}#${encodeURIComponent(
proxy.name, proxy.name,
)}`; )}`;
break; break;
@ -324,16 +374,54 @@ export default function URI_Producer() {
: proxy.alpn.join(','), : proxy.alpn.join(','),
)}`; )}`;
} }
const trojanIsReality = proxy['reality-opts'];
let trojanSid = '';
let trojanPbk = '';
let trojanSpx = '';
let trojanSecurity = '';
let trojanMode = '';
let trojanExtra = '';
if (trojanIsReality) {
trojanSecurity = `&security=reality`;
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
trojanSid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;
}
if (proxy._extra) {
trojanExtra = `&extra=${encodeURIComponent(
proxy._extra,
)}`;
}
if (proxy._mode) {
trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${ result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port proxy.port
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${ }?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : '' proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}${trojanAlpn}${trojanFp}#${encodeURIComponent( }${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
proxy.name, proxy.name,
)}`; )}`;
break; break;
case 'hysteria2': case 'hysteria2':
let hysteria2params = []; let hysteria2params = [];
if (proxy['hop-interval']) {
hysteria2params.push(
`hop-interval=${proxy['hop-interval']}`,
);
}
if (proxy['keepalive']) {
hysteria2params.push(`keepalive=${proxy['keepalive']}`);
}
if (proxy['skip-cert-verify']) { if (proxy['skip-cert-verify']) {
hysteria2params.push(`insecure=1`); hysteria2params.push(`insecure=1`);
} }
@ -413,7 +501,7 @@ export default function URI_Producer() {
hysteriaParams.push(`obfsParam=${proxy[key]}`); hysteriaParams.push(`obfsParam=${proxy[key]}`);
} else if (['sni'].includes(key)) { } else if (['sni'].includes(key)) {
hysteriaParams.push(`peer=${proxy[key]}`); hysteriaParams.push(`peer=${proxy[key]}`);
} else if (proxy[key]) { } else if (proxy[key] && !/^_/i.test(key)) {
hysteriaParams.push( hysteriaParams.push(
`${i}=${encodeURIComponent(proxy[key])}`, `${i}=${encodeURIComponent(proxy[key])}`,
); );
@ -440,6 +528,7 @@ export default function URI_Producer() {
'password', 'password',
'server', 'server',
'port', 'port',
'tls',
].includes(key) ].includes(key)
) { ) {
const i = key.replace(/-/, '_'); const i = key.replace(/-/, '_');
@ -468,10 +557,19 @@ export default function URI_Producer() {
['disable-sni', 'reduce-rtt'].includes(key) && ['disable-sni', 'reduce-rtt'].includes(key) &&
proxy[key] proxy[key]
) { ) {
tuicParams.push(`${i}=1`); tuicParams.push(`${i.replace(/-/g, '_')}=1`);
} else if (proxy[key]) { } else if (
['congestion-controller'].includes(key)
) {
tuicParams.push( tuicParams.push(
`${i}=${encodeURIComponent(proxy[key])}`, `congestion_control=${proxy[key]}`,
);
} else if (proxy[key] && !/^_/i.test(key)) {
tuicParams.push(
`${i.replace(
/-/g,
'_',
)}=${encodeURIComponent(proxy[key])}`,
); );
} }
} }
@ -486,6 +584,54 @@ export default function URI_Producer() {
)}`; )}`;
} }
break; break;
case 'anytls':
let anytlsParams = [];
Object.keys(proxy).forEach((key) => {
if (
![
'name',
'type',
'password',
'server',
'port',
'tls',
].includes(key)
) {
const i = key.replace(/-/, '_');
if (['alpn'].includes(key)) {
if (proxy[key]) {
anytlsParams.push(
`${i}=${encodeURIComponent(
Array.isArray(proxy[key])
? proxy[key][0]
: proxy[key],
)}`,
);
}
} else if (['skip-cert-verify'].includes(key)) {
if (proxy[key]) {
anytlsParams.push(`insecure=1`);
}
} else if (['udp'].includes(key)) {
if (proxy[key]) {
anytlsParams.push(`udp=1`);
}
} else if (proxy[key] && !/^_/i.test(key)) {
anytlsParams.push(
`${i.replace(/-/g, '_')}=${encodeURIComponent(
proxy[key],
)}`,
);
}
}
});
result = `anytls://${encodeURIComponent(proxy.password)}@${
proxy.server
}:${proxy.port}/?${anytlsParams.join('&')}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'wireguard': case 'wireguard':
let wireguardParams = []; let wireguardParams = [];
@ -507,7 +653,7 @@ export default function URI_Producer() {
if (proxy[key]) { if (proxy[key]) {
wireguardParams.push(`${key}=1`); wireguardParams.push(`${key}=1`);
} }
} else if (proxy[key]) { } else if (proxy[key] && !/^_/i.test(key)) {
wireguardParams.push( wireguardParams.push(
`${key}=${encodeURIComponent(proxy[key])}`, `${key}=${encodeURIComponent(proxy[key])}`,
); );

View File

@ -11,6 +11,7 @@
* @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';
import $ from '@/core/app';
console.log( console.log(
` `
@ -18,7 +19,6 @@ console.log(
`, `,
); );
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import serve from '@/restful'; import serve from '@/restful';

View File

@ -84,12 +84,15 @@ async function doSync() {
const files = {}; const files = {};
try { try {
const valid = [];
const invalid = []; const invalid = [];
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const subNames = []; const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => { allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) { if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') { if (artifact.type === 'subscription') {
const subName = artifact.source; const subName = artifact.source;
const sub = findByName(allSubs, subName); const sub = findByName(allSubs, subName);
@ -110,6 +113,13 @@ async function doSync() {
} }
}); });
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) { if (subNames.length > 0) {
await Promise.all( await Promise.all(
subNames.map(async (subName) => { subNames.map(async (subName) => {
@ -156,27 +166,46 @@ async function doSync() {
files[encodeURIComponent(artifact.name)] = { files[encodeURIComponent(artifact.name)] = {
content: output, content: output,
}; };
valid.push(artifact.name);
} }
} catch (e) { } catch (e) {
$.error( $.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`, `生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
); );
invalid.push(artifact.name); invalid.push(artifact.name);
} }
}), }),
); );
if (invalid.length > 0) { $.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
if (valid.length === 0) {
throw new Error( throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`, `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
); );
} }
const resp = await syncToGist(files); const resp = await syncToGist(files);
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) { for (const artifact of allArtifacts) {
if (artifact.sync) { if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
let files = body.files; let files = body.files;
@ -204,9 +233,18 @@ async function doSync() {
} }
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.notify('🌍 Sub-Store', '全部订阅同步成功!'); $.info('上传配置成功');
if (invalid.length > 0) {
$.notify(
'🌍 Sub-Store',
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.notify('🌍 Sub-Store', '同步配置完成');
}
} catch (e) { } catch (e) {
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${e.message ?? e}`); $.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`);
$.error(`无法同步订阅配置到 Gist原因${e}`); $.error(`无法同步配置到 Gist原因${e}`);
} }
} }

View File

@ -14,10 +14,12 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
` `
Sub-Store -- v${version} Sub-Store -- v${version}
Loon -- ${$loon}
`, `,
); );
const build = $loon.match(/\((\d+)\)$/)?.[1];
let arg; let arg;
if (typeof $argument != 'undefined') { if (typeof $argument != 'undefined') {
arg = Object.fromEntries( arg = Object.fromEntries(
@ -26,24 +28,29 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
} else { } else {
arg = {}; arg = {};
} }
console.log(`arg: ${JSON.stringify(arg)}`);
const RESOURCE_TYPE = { const RESOURCE_TYPE = {
PROXY: 1, PROXY: 1,
RULE: 2, RULE: 2,
}; };
if (!arg.resourceUrlOnly) {
result = resource; result = resource;
}
if (resourceType === RESOURCE_TYPE.PROXY) { if (resourceType === RESOURCE_TYPE.PROXY) {
if (!arg.resourceUrlOnly) {
try { try {
let proxies = ProxyUtils.parse(resource); let proxies = ProxyUtils.parse(resource);
result = ProxyUtils.produce(proxies, 'Loon', undefined, { result = ProxyUtils.produce(proxies, 'Loon', undefined, {
'include-unsupported-proxy': arg?.includeUnsupportedProxy, 'include-unsupported-proxy':
arg?.includeUnsupportedProxy || build >= 842,
}); });
} catch (e) { } catch (e) {
console.log('解析器: 使用 resource 出现错误'); console.log('解析器: 使用 resource 出现错误');
console.log(e.message ?? e); console.log(e.message ?? e);
} }
}
if ((!result || /^\s*$/.test(result)) && resourceUrl) { if ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`); console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
try { try {
@ -59,19 +66,22 @@ let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
); );
let proxies = ProxyUtils.parse(raw); let proxies = ProxyUtils.parse(raw);
result = ProxyUtils.produce(proxies, 'Loon', undefined, { result = ProxyUtils.produce(proxies, 'Loon', undefined, {
'include-unsupported-proxy': arg?.includeUnsupportedProxy, 'include-unsupported-proxy':
arg?.includeUnsupportedProxy || build >= 842,
}); });
} catch (e) { } catch (e) {
console.log(e.message ?? e); console.log(e.message ?? e);
} }
} }
} else if (resourceType === RESOURCE_TYPE.RULE) { } else if (resourceType === RESOURCE_TYPE.RULE) {
if (!arg.resourceUrlOnly) {
try { try {
const rules = RuleUtils.parse(resource); const rules = RuleUtils.parse(resource);
result = RuleUtils.produce(rules, 'Loon'); result = RuleUtils.produce(rules, 'Loon');
} catch (e) { } catch (e) {
console.log(e.message ?? e); console.log(e.message ?? e);
} }
}
if ((!result || /^\s*$/.test(result)) && resourceUrl) { if ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`); console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);
try { try {

View File

@ -1,8 +1,9 @@
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; import { COLLECTIONS_KEY, ARTIFACTS_KEY, FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors'; import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
import { formatDateTime } from '@/utils';
export default function register($app) { export default function register($app) {
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY); if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
@ -50,11 +51,25 @@ function createCollection(req, res) {
function getCollection(req, res) { function getCollection(req, res) {
let { name } = req.params; let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name); const collection = findByName(allCols, name);
if (collection) { if (collection) {
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(
`sub-store_collection_${name}_${formatDateTime(
new Date(),
)}.json`,
)}"`,
)
.send(JSON.stringify(collection));
} else {
success(res, collection); success(res, collection);
}
} else { } else {
failed( failed(
res, res,
@ -91,7 +106,18 @@ function updateCollection(req, res) {
artifact.source = newCol.name; artifact.source = newCol.name;
} }
} }
// update all files referring this collection
const allFiles = $.read(FILES_KEY) || [];
for (const file of allFiles) {
if (
file.sourceType === 'collection' &&
file.sourceName === oldCol.name
) {
file.sourceName = newCol.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
} }
updateByName(allCols, name, newCol); updateByName(allCols, name, newCol);

View File

@ -1,8 +1,11 @@
import { getPlatformFromHeaders } from '@/utils/user-agent'; import {
getPlatformFromHeaders,
shouldIncludeUnsupportedProxy,
} from '@/utils/user-agent';
import { ProxyUtils } from '@/core/proxy-utils'; import { ProxyUtils } from '@/core/proxy-utils';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants'; import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database'; import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import $ from '@/core/app'; import $ from '@/core/app';
import { failed } from '@/restful/response'; import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors'; import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@ -13,11 +16,44 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env'; import env from '@/utils/env';
export default function register($app) { export default function register($app) {
$app.get('/share/col/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/share/col/:name', downloadCollection); $app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/share/sub/:name', downloadSubscription); $app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/download/collection/:name', downloadCollection); $app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/download/:name', downloadSubscription); $app.get('/download/:name', downloadSubscription);
$app.get( $app.get(
'/download/collection/:name/api/v1/server/details', '/download/collection/:name/api/v1/server/details',
async (req, res) => { async (req, res) => {
@ -59,11 +95,9 @@ async function downloadSubscription(req, res) {
const platform = const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON'; req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info( $.info(
`正在下载订阅:${name}\n请求 User-Agent: ${ `正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
req.headers['user-agent'] || req.headers['User-Agent']
}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
); );
let { let {
url, url,
@ -77,7 +111,17 @@ async function downloadSubscription(req, res) {
proxy, proxy,
noCache, noCache,
} = req.query; } = req.query;
let $options = {}; let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) { if (req.query.$options) {
try { try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
@ -98,6 +142,14 @@ async function downloadSubscription(req, res) {
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`); $.info(`指定远程订阅 URL: ${url}`);
if (!/^https?:\/\//.test(url)) {
content = url;
$.info(`URL 不是链接,视为本地订阅`);
}
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
} }
if (proxy) { if (proxy) {
proxy = decodeURIComponent(proxy); proxy = decodeURIComponent(proxy);
@ -107,10 +159,7 @@ async function downloadSubscription(req, res) {
ua = decodeURIComponent(ua); ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`); $.info(`指定远程订阅 User-Agent: ${ua}`);
} }
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (mergeSources) { if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources); mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`); $.info(`指定合并来源: ${mergeSources}`);
@ -125,7 +174,19 @@ async function downloadSubscription(req, res) {
} }
if (includeUnsupportedProxy) { if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy); includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`); $.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
} }
if (useMihomoExternal) { if (useMihomoExternal) {
@ -140,6 +201,13 @@ async function downloadSubscription(req, res) {
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
if (sub) { if (sub) {
try { try {
const passThroughUA = sub.passThroughUA;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
);
ua = reqUA;
}
let output = await produceArtifact({ let output = await produceArtifact({
type: 'subscription', type: 'subscription',
name, name,
@ -191,7 +259,7 @@ async function downloadSubscription(req, res) {
} }
} }
} }
if (!$arguments.noFlow) { if (!$arguments.noFlow && /^https?/.test(url)) {
// forward flow headers // forward flow headers
flowInfo = await getFlowHeaders( flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url, $arguments?.insecure ? `${url}#insecure` : url,
@ -201,7 +269,10 @@ async function downloadSubscription(req, res) {
$arguments.flowUrl, $arguments.flowUrl,
); );
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
} }
} }
} catch (err) { } catch (err) {
@ -235,7 +306,9 @@ async function downloadSubscription(req, res) {
} }
res.set( res.set(
'subscription-userinfo', 'subscription-userinfo',
normalizeFlowHeader(
[subUserInfo, flowInfo].filter((i) => i).join(';'), [subUserInfo, flowInfo].filter((i) => i).join(';'),
),
); );
} }
@ -299,11 +372,9 @@ async function downloadCollection(req, res) {
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name); const collection = findByName(allCols, name);
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info( $.info(
`正在下载组合订阅:${name}\n请求 User-Agent: ${ `正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
req.headers['user-agent'] || req.headers['User-Agent']
}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
); );
let { let {
@ -315,7 +386,17 @@ async function downloadCollection(req, res) {
noCache, noCache,
} = req.query; } = req.query;
let $options = {}; let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) { if (req.query.$options) {
try { try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
@ -350,7 +431,18 @@ async function downloadCollection(req, res) {
if (includeUnsupportedProxy) { if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy); includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`); $.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
} }
if (useMihomoExternal) { if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`); $.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
@ -374,6 +466,7 @@ async function downloadCollection(req, res) {
$options, $options,
proxy, proxy,
noCache, noCache,
ua: reqUA,
}); });
let subUserInfoOfSub; let subUserInfoOfSub;
// forward flow header from the first subscription in this collection // forward flow header from the first subscription in this collection
@ -413,7 +506,7 @@ async function downloadCollection(req, res) {
} }
} }
} }
if (!$arguments.noFlow) { if (!$arguments.noFlow && /^https?:/.test(url)) {
subUserInfoOfSub = await getFlowHeaders( subUserInfoOfSub = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url, $arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent, $arguments.flowUserAgent,
@ -479,13 +572,15 @@ async function downloadCollection(req, res) {
} else { } else {
subUserInfoOfCol = collection.subUserinfo; subUserInfoOfCol = collection.subUserinfo;
} }
const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
if (subUserInfo) {
res.set( res.set(
'subscription-userinfo', 'subscription-userinfo',
[subUserInfoOfCol, subUserInfoOfSub] normalizeFlowHeader(subUserInfo),
.filter((i) => i)
.join('; '),
); );
}
if (platform === 'JSON') { if (platform === 'JSON') {
if (resultFormat === 'nezha') { if (resultFormat === 'nezha') {
output = nezhaTransform(output); output = nezhaTransform(output);

View File

@ -1,6 +1,6 @@
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow'; import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import { FILES_KEY } from '@/constants'; import { FILES_KEY, ARTIFACTS_KEY } from '@/constants';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import $ from '@/core/app'; import $ from '@/core/app';
import { import {
@ -9,6 +9,7 @@ import {
InternalServerError, InternalServerError,
} from '@/restful/errors'; } from '@/restful/errors';
import { produceArtifact } from '@/restful/sync'; import { produceArtifact } from '@/restful/sync';
import { formatDateTime } from '@/utils';
export default function register($app) { export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY); if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
@ -51,8 +52,8 @@ function createFile(req, res) {
async function getFile(req, res) { async function getFile(req, res) {
let { name } = req.params; let { name } = req.params;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(`正在下载文件:${name}`); $.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`);
let { let {
url, url,
subInfoUrl, subInfoUrl,
@ -63,8 +64,19 @@ async function getFile(req, res) {
ignoreFailedRemoteFile, ignoreFailedRemoteFile,
proxy, proxy,
noCache, noCache,
produceType,
} = req.query; } = req.query;
let $options = {}; let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) { if (req.query.$options) {
try { try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}` // 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
@ -117,6 +129,10 @@ async function getFile(req, res) {
if (noCache) { if (noCache) {
$.info(`指定不使用缓存: ${noCache}`); $.info(`指定不使用缓存: ${noCache}`);
} }
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
const allFiles = $.read(FILES_KEY); const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name); const file = findByName(allFiles, name);
@ -133,6 +149,8 @@ async function getFile(req, res) {
$options, $options,
proxy, proxy,
noCache, noCache,
produceType,
all: true,
}); });
try { try {
@ -146,7 +164,10 @@ async function getFile(req, res) {
proxy || file.proxy, proxy || file.proxy,
); );
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
} }
} }
} catch (err) { } catch (err) {
@ -164,9 +185,15 @@ async function getFile(req, res) {
)}`, )}`,
); );
} }
res.set('Content-Type', 'text/plain; charset=utf-8').send( res.set('Content-Type', 'text/plain; charset=utf-8');
output ?? '', if (output?.$options?._res?.headers) {
Object.entries(output.$options._res.headers).forEach(
([key, value]) => {
res.set(key, value);
},
); );
}
res.send(output?.$content ?? '');
} catch (err) { } catch (err) {
$.notify( $.notify(
`🌍 Sub-Store 下载文件失败`, `🌍 Sub-Store 下载文件失败`,
@ -197,11 +224,25 @@ async function getFile(req, res) {
} }
function getWholeFile(req, res) { function getWholeFile(req, res) {
let { name } = req.params; let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name); name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY); const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name); const file = findByName(allFiles, name);
if (file) { if (file) {
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(
`sub-store_file_${name}_${formatDateTime(
new Date(),
)}.json`,
)}"`,
)
.send(JSON.stringify(file));
} else {
success(res, file); success(res, file);
}
} else { } else {
failed( failed(
res, res,
@ -227,6 +268,20 @@ function updateFile(req, res) {
}; };
$.info(`正在更新文件:${name}...`); $.info(`正在更新文件:${name}...`);
if (name !== newFile.name) {
// update all artifacts referring this collection
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'file' &&
artifact.source === oldFile.name
) {
artifact.source = newFile.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allFiles, name, newFile); updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY); $.write(allFiles, FILES_KEY);
success(res, newFile); success(res, newFile);

View File

@ -1,8 +1,8 @@
import express from '@/vendor/express'; import express from '@/vendor/express';
import $ from '@/core/app'; import $ from '@/core/app';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import download from '@/utils/download'; import download, { downloadFile } from '@/utils/download';
import { syncArtifacts } from '@/restful/sync'; import { syncArtifacts, produceArtifact } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs'; import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants'; import { TOKENS_KEY } from '@/constants';
@ -29,6 +29,77 @@ export default function serve() {
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::'; host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
} }
const $app = express({ substore: $, port, host }); const $app = express({ substore: $, port, host });
if ($.env.isNode) {
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
if (be_prefix || be_merge) {
if (!fe_be_path.startsWith('/')) {
throw new Error(
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
);
}
if (be_merge) {
$.info(`[BACKEND] MERGE mode is [ON].`);
$.info(`[BACKEND && FRONTEND] ${host}:${port}`);
}
$.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);
$app.use((req, res, next) => {
if (req.path.startsWith(fe_be_path)) {
req.url = req.url.replace(fe_be_path, '') || '/';
if (be_merge && req.url.startsWith('/api/')) {
req.query['share'] = 'true';
}
next();
return;
}
const pathname =
decodeURIComponent(req._parsedUrl.pathname) || '/';
if (
be_merge &&
req.path.startsWith('/share/') &&
req.query.token
) {
if (req.method.toLowerCase() !== 'get') {
res.status(405).send('Method not allowed');
return;
}
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
`/share/${t.type}/${t.name}` === pathname &&
(t.exp == null || t.exp > Date.now()),
);
if (token) {
next();
return;
}
}
if (be_merge && fe_path && req.path.indexOf('/', 1) == -1) {
if (req.path.indexOf('.') == -1) {
req.url = '/index.html';
}
const express_ = eval(`require("express")`);
const mime_ = eval(`require("mime-types")`);
const path_ = eval(`require("path")`);
const staticFileMiddleware = express_.static(fe_path, {
setHeaders: (res, path) => {
const type = mime_.contentType(path_.extname(path));
if (type) {
res.set('Content-Type', type);
}
},
});
staticFileMiddleware(req, res, next);
return;
}
res.status(403).end('Forbbiden');
return;
});
}
}
// register routes // register routes
registerCollectionRoutes($app); registerCollectionRoutes($app);
registerSubscriptionRoutes($app); registerSubscriptionRoutes($app);
@ -75,6 +146,39 @@ export default function serve() {
// 'Asia/Shanghai' // timeZone // 'Asia/Shanghai' // timeZone
); );
} }
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
if (produce_cron) {
$.info(`[PRODUCE CRON] ${produce_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
produce_cron.split(/\s*;\s*/).map((item) => {
const [cron, type, name] = item.split(/\s*,\s*/);
new CronJob(
cron.trim(),
async function () {
try {
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} started`,
);
await produceArtifact({ type, name });
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} finished`,
);
} catch (e) {
$.error(
`[PRODUCE CRON] ${type} ${name} ${cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
});
}
const backend_download_cron = eval( const backend_download_cron = eval(
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON', 'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
); );
@ -131,6 +235,60 @@ export default function serve() {
// 'Asia/Shanghai' // timeZone // 'Asia/Shanghai' // timeZone
); );
} }
const mmdb_cron = eval('process.env.SUB_STORE_MMDB_CRON');
const countryFile = eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
const countryUrl = eval('process.env.SUB_STORE_MMDB_COUNTRY_URL');
const asnFile = eval('process.env.SUB_STORE_MMDB_ASN_PATH');
const asnUrl = eval('process.env.SUB_STORE_MMDB_ASN_URL');
if (mmdb_cron && ((countryFile && countryUrl) || (asnFile && asnUrl))) {
$.info(`[MMDB CRON] ${mmdb_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
mmdb_cron,
async function () {
try {
$.info(`[MMDB CRON] ${mmdb_cron} started`);
if (countryFile && countryUrl) {
try {
$.info(
`[MMDB CRON] downloading ${countryUrl} to ${countryFile}`,
);
await downloadFile(countryUrl, countryFile);
} catch (e) {
$.error(
`[MMDB CRON] ${countryUrl} download failed: ${
e.message ?? e
}`,
);
}
}
if (asnFile && asnUrl) {
try {
$.info(
`[MMDB CRON] downloading ${asnUrl} to ${asnFile}`,
);
await downloadFile(asnUrl, asnFile);
} catch (e) {
$.error(
`[MMDB CRON] ${asnUrl} download failed: ${
e.message ?? e
}`,
);
}
}
$.info(`[MMDB CRON] ${mmdb_cron} finished`);
} catch (e) {
$.error(
`[MMDB CRON] ${mmdb_cron} error: ${e.message ?? e}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const path = eval(`require("path")`); const path = eval(`require("path")`);
const fs = eval(`require("fs")`); const fs = eval(`require("fs")`);
const data_url = eval('process.env.SUB_STORE_DATA_URL'); const data_url = eval('process.env.SUB_STORE_DATA_URL');
@ -142,7 +300,8 @@ export default function serve() {
const fe_abs_path = path.resolve( const fe_abs_path = path.resolve(
fe_path || path.join(__dirname, 'frontend'), fe_path || path.join(__dirname, 'frontend'),
); );
if (fe_path) { const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
if (fe_path && !be_merge) {
try { try {
fs.accessSync(path.join(fe_abs_path, 'index.html')); fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) { } catch (e) {
@ -161,11 +320,15 @@ export default function serve() {
const staticFileMiddleware = express_.static(fe_path); const staticFileMiddleware = express_.static(fe_path);
let be_share_rewrite = '/share/:type/:name';
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/'; let be_api = '/api/';
let be_download = '/download/'; let be_download = '/download/';
let be_share = '/share/';
let be_download_rewrite = '';
let be_api_rewrite = '';
let be_share_rewrite = `${be_share}:type/:name`;
let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')
? fe_be_path
: '';
if (fe_be_path) { if (fe_be_path) {
if (!fe_be_path.startsWith('/')) { if (!fe_be_path.startsWith('/')) {
throw new Error( throw new Error(
@ -182,9 +345,9 @@ export default function serve() {
app.use( app.use(
be_share_rewrite, be_share_rewrite,
createProxyMiddleware({ createProxyMiddleware({
target: `http://127.0.0.1:${port}`, target: `http://127.0.0.1:${port}${prefix}`,
changeOrigin: true, changeOrigin: true,
pathRewrite: (path, req) => { pathRewrite: async (path, req) => {
if (req.method.toLowerCase() !== 'get') if (req.method.toLowerCase() !== 'get')
throw new Error('Method not allowed'); throw new Error('Method not allowed');
const tokens = $.read(TOKENS_KEY) || []; const tokens = $.read(TOKENS_KEY) || [];
@ -196,34 +359,26 @@ export default function serve() {
(t.exp == null || t.exp > Date.now()), (t.exp == null || t.exp > Date.now()),
); );
if (!token) throw new Error('Forbbiden'); if (!token) throw new Error('Forbbiden');
return path; return req.originalUrl;
}, },
}), }),
); );
app.use( app.use(
be_api_rewrite, be_api_rewrite,
createProxyMiddleware({ createProxyMiddleware({
target: `http://127.0.0.1:${port}`, target: `http://127.0.0.1:${port}${prefix}${be_api}`,
pathRewrite: (path) => { pathRewrite: async (path) => {
const newPath = path.startsWith(be_api_rewrite) return path.includes('?')
? path.replace(be_api_rewrite, be_api) ? `${path}&share=true`
: path; : `${path}?share=true`;
return newPath.includes('?')
? `${newPath}&share=true`
: `${newPath}?share=true`;
}, },
}), }),
); );
app.use( app.use(
be_download_rewrite, be_download_rewrite,
createProxyMiddleware({ createProxyMiddleware({
target: `http://127.0.0.1:${port}`, target: `http://127.0.0.1:${port}${prefix}${be_download}`,
changeOrigin: true, changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_download_rewrite)
? path.replace(be_download_rewrite, be_download)
: path;
},
}), }),
); );
} }
@ -243,10 +398,10 @@ export default function serve() {
$.info(`[FRONTEND] ${fe_address}:${fe_port}`); $.info(`[FRONTEND] ${fe_address}:${fe_port}`);
if (fe_be_path) { if (fe_be_path) {
$.info( $.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`, `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,
); );
$.info( $.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`, `[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> ${host}:${port}${prefix}${be_download}`,
); );
$.info( $.info(
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`, `[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,

View File

@ -14,6 +14,7 @@ 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'; import env from '@/utils/env';
import { formatDateTime } from '@/utils';
export default function register($app) { export default function register($app) {
// utils // utils
@ -27,7 +28,9 @@ export default function register($app) {
res.set('content-type', 'application/json') res.set('content-type', 'application/json')
.set( .set(
'content-disposition', 'content-disposition',
'attachment; filename="sub-store.json"', `attachment; filename="${encodeURIComponent(
`sub-store_data_${formatDateTime(new Date())}.json`,
)}"`,
) )
.send( .send(
$.env.isNode $.env.isNode
@ -46,11 +49,17 @@ export default function register($app) {
success(res); success(res);
}); });
if (ENV().isNode) {
$app.get('/', getEnv);
} else {
// Redirect sub.store to vercel webpage // Redirect sub.store to vercel webpage
$app.get('/', async (req, res) => { $app.get('/', async (req, res) => {
// 302 redirect // 302 redirect
res.set('location', 'https://sub-store.vercel.app/').status(302).end(); res.set('location', 'https://sub-store.vercel.app/')
.status(302)
.end();
}); });
}
// handle preflight request for QX // handle preflight request for QX
if (ENV().isQX) { if (ENV().isQX) {
@ -68,7 +77,19 @@ function getEnv(req, res) {
if (req.query.share) { if (req.query.share) {
env.feature.share = true; env.feature.share = true;
} }
success(res, env); res.set('Content-Type', 'application/json;charset=UTF-8').send(
JSON.stringify(
{
status: 'success',
data: {
guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',
...env,
},
},
null,
2,
),
);
} }
async function refresh(_, res) { async function refresh(_, res) {
@ -98,6 +119,21 @@ async function gistBackupAction(action) {
const updated = settings.syncTime; const updated = settings.syncTime;
switch (action) { switch (action) {
case 'upload': case 'upload':
try {
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
$.info(`下载备份, 与本地内容对比...`);
const onlineContent = await gist.download(
GIST_BACKUP_FILE_NAME,
);
if (onlineContent === content) {
$.info(`内容一致, 无需上传备份`);
return;
}
} catch (error) {
$.error(`${error.message ?? error}`);
}
// update syncTime // update syncTime
settings.syncTime = new Date().getTime(); settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);

View File

@ -15,7 +15,8 @@ export default function register($app) {
async function previewFile(req, res) { async function previewFile(req, res) {
try { try {
const file = req.body; const file = req.body;
let content; let content = '';
if (file.type !== 'mihomoProfile') {
if ( if (
file.source === 'local' && file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources) !['localFirst', 'remoteFirst'].includes(file.mergeSources)
@ -30,7 +31,12 @@ async function previewFile(req, res) {
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, file.ua); return await download(
url,
file.ua,
undefined,
file.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@ -41,22 +47,30 @@ async function previewFile(req, res) {
}), }),
); );
if ( if (Object.keys(errors).length > 0) {
!file.ignoreFailedRemoteFile && if (!file.ignoreFailedRemoteFile) {
Object.keys(errors).length > 0
) {
throw new Error( throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join( `文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (file.ignoreFailedRemoteFile === 'enabled') {
$.notify(
`🌍 Sub-Store 预览文件失败`,
`${file.name}`,
`远程文件 ${Object.keys(errors).join(
', ', ', ',
)} 发生错误, 请查看日志`, )} 发生错误, 请查看日志`,
); );
} }
}
if (file.mergeSources === 'localFirst') { if (file.mergeSources === 'localFirst') {
content.unshift(file.content); content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') { } else if (file.mergeSources === 'remoteFirst') {
content.push(file.content); content.push(file.content);
} }
} }
}
// parse proxies // parse proxies
const files = (Array.isArray(content) ? content : [content]).flat(); const files = (Array.isArray(content) ? content : [content]).flat();
let filesContent = files let filesContent = files
@ -67,7 +81,7 @@ async function previewFile(req, res) {
const processed = const processed =
Array.isArray(file.process) && file.process.length > 0 Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process( ? await ProxyUtils.process(
{ $files: files, $content: filesContent }, { $files: files, $content: filesContent, $file: file },
file.process, file.process,
) )
: { $content: filesContent, $files: files }; : { $content: filesContent, $files: files };
@ -129,12 +143,22 @@ async function compareSub(req, res) {
}), }),
); );
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) { if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error( throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join( `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ', ', ',
)} 发生错误, 请查看日志`, )} 发生错误, 请查看日志`,
); );
} else if (sub.ignoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 预览订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
} }
if (sub.mergeSources === 'localFirst') { if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content); content.unshift(sub.content);
@ -237,15 +261,25 @@ async function compareCollection(req, res) {
} }
}), }),
); );
if (
!sub.ignoreFailedRemoteSub && if (Object.keys(errors).length > 0) {
Object.keys(errors).length > 0 if (!sub.ignoreFailedRemoteSub) {
) {
throw new Error( throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys( `订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors, errors,
).join(', ')} 发生错误, 请查看日志`, ).join(', ')} 发生错误, 请查看日志`,
); );
} else if (
sub.ignoreFailedRemoteSub === 'enabled'
) {
$.notify(
`🌍 Sub-Store 预览订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
} }
if (sub.mergeSources === 'localFirst') { if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content); raw.unshift(sub.content);
@ -282,15 +316,23 @@ async function compareCollection(req, res) {
} }
}), }),
); );
if (
!collection.ignoreFailedRemoteSub && if (Object.keys(errors).length > 0) {
Object.keys(errors).length > 0 if (!collection.ignoreFailedRemoteSub) {
) {
throw new Error( throw new Error(
`组合订阅 ${collection.name} 的子订阅 ${Object.keys( `组合订阅 ${collection.name} 的子订阅 ${Object.keys(
errors, errors,
).join(', ')} 发生错误, 请查看日志`, ).join(', ')} 发生错误, 请查看日志`,
); );
} else if (collection.ignoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 预览组合订阅失败`,
`${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(

View File

@ -134,11 +134,15 @@ export async function updateArtifactStore() {
settings.artifactStore = url; settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID'; settings.artifactStoreStatus = 'VALID';
} else { } else {
$.error(`找不到 Sub-Store Gist`); $.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);
settings.artifactStoreStatus = 'NOT FOUND'; settings.artifactStoreStatus = 'NOT FOUND';
} }
} catch (err) { } catch (err) {
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`); $.error(
`查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${
err.message ?? err
}`,
);
settings.artifactStoreStatus = 'ERROR'; settings.artifactStoreStatus = 'ERROR';
} }
$.write(settings, SETTINGS_KEY); $.write(settings, SETTINGS_KEY);

View File

@ -5,7 +5,12 @@ import {
RequestInvalidError, RequestInvalidError,
} 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,
FILES_KEY,
} from '@/constants';
import { import {
getFlowHeaders, getFlowHeaders,
parseFlowHeaders, parseFlowHeaders,
@ -13,6 +18,7 @@ import {
} from '@/utils/flow'; } from '@/utils/flow';
import { success, failed } from './response'; import { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
import { formatDateTime } from '@/utils';
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY); if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
@ -134,7 +140,7 @@ async function getFlowInfo(req, res) {
} }
} }
} }
if ($arguments.noFlow) { if ($arguments.noFlow || !/^https?/.test(url)) {
failed( failed(
res, res,
new RequestInvalidError( new RequestInvalidError(
@ -264,7 +270,11 @@ function getSubscription(req, res) {
res.set('content-type', 'application/json') res.set('content-type', 'application/json')
.set( .set(
'content-disposition', 'content-disposition',
`attachment; filename="${encodeURIComponent(name)}.json"`, `attachment; filename="${encodeURIComponent(
`sub-store_subscription_${name}_${formatDateTime(
new Date(),
)}.json`,
)}"`,
) )
.send(JSON.stringify(sub)); .send(JSON.stringify(sub));
} else { } else {
@ -315,9 +325,20 @@ function updateSubscription(req, res) {
artifact.source = sub.name; artifact.source = sub.name;
} }
} }
// update all files referring this subscription
const allFiles = $.read(FILES_KEY) || [];
for (const file of allFiles) {
if (
file.sourceType === 'subscription' &&
file.sourceName == name
) {
file.sourceName = sub.name;
}
}
$.write(allCols, COLLECTIONS_KEY); $.write(allCols, COLLECTIONS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
} }
updateByName(allSubs, name, newSub); updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY); $.write(allSubs, SUBS_KEY);

View File

@ -40,10 +40,11 @@ async function produceArtifact({
$options, $options,
proxy, proxy,
noCache, noCache,
all,
}) { }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
if (type === 'subscription') { if (['subscription', 'sub'].includes(type)) {
let sub; let sub;
if (name) { if (name) {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
@ -73,7 +74,7 @@ async function produceArtifact({
proxy || sub.proxy, proxy || sub.proxy,
undefined, undefined,
awaitCustomCache, awaitCustomCache,
noCache, noCache || sub.noCache,
true, true,
); );
} catch (err) { } catch (err) {
@ -89,12 +90,23 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub; subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
} }
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
if (Object.keys(errors).length > 0) {
if (!subIgnoreFailedRemoteSub) {
throw new Error( throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join( `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ', ', ',
)} 发生错误, 请查看日志`, )} 发生错误, 请查看日志`,
); );
} else if (subIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
} }
if (mergeSources === 'localFirst') { if (mergeSources === 'localFirst') {
raw.unshift(content); raw.unshift(content);
@ -122,7 +134,7 @@ async function produceArtifact({
proxy || sub.proxy, proxy || sub.proxy,
undefined, undefined,
awaitCustomCache, awaitCustomCache,
noCache, noCache || sub.noCache,
true, true,
); );
} catch (err) { } catch (err) {
@ -138,12 +150,23 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub; subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
} }
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
if (Object.keys(errors).length > 0) {
if (!subIgnoreFailedRemoteSub) {
throw new Error( throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join( `订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ', ', ',
)} 发生错误, 请查看日志`, )} 发生错误, 请查看日志`,
); );
} else if (subIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
} }
if (sub.mergeSources === 'localFirst') { if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content); raw.unshift(sub.content);
@ -151,6 +174,9 @@ async function produceArtifact({
raw.push(sub.content); raw.push(sub.content);
} }
} }
if (produceType === 'raw') {
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
}
// parse proxies // parse proxies
let proxies = (Array.isArray(raw) ? raw : [raw]) let proxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i)) .map((i) => ProxyUtils.parse(i))
@ -190,7 +216,7 @@ async function produceArtifact({
} }
// produce // produce
return ProxyUtils.produce(proxies, platform, produceType, produceOpts); return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') { } else if (['collection', 'col'].includes(type)) {
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name); const collection = findByName(allCols, name);
@ -216,6 +242,14 @@ async function produceArtifact({
await Promise.all( await Promise.all(
subnames.map(async (name) => { subnames.map(async (name) => {
const sub = findByName(allSubs, name); const sub = findByName(allSubs, name);
const passThroughUA = sub.passThroughUA;
let reqUA = sub.ua;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,
);
reqUA = ua;
}
try { try {
$.info(`正在处理子订阅:${sub.name}...`); $.info(`正在处理子订阅:${sub.name}...`);
let raw; let raw;
@ -237,14 +271,14 @@ async function produceArtifact({
try { try {
return await download( return await download(
url, url,
sub.ua, reqUA,
undefined, undefined,
proxy || proxy ||
sub.proxy || sub.proxy ||
collection.proxy, collection.proxy,
undefined, undefined,
undefined, undefined,
noCache, noCache || sub.noCache,
true, true,
); );
} catch (err) { } catch (err) {
@ -256,15 +290,25 @@ async function produceArtifact({
} }
}), }),
); );
if (
!sub.ignoreFailedRemoteSub && if (Object.keys(errors).length > 0) {
Object.keys(errors).length > 0 if (!sub.ignoreFailedRemoteSub) {
) {
throw new Error( throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys( `订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors, errors,
).join(', ')} 发生错误, 请查看日志`, ).join(', ')} 发生错误, 请查看日志`,
); );
} else if (
sub.ignoreFailedRemoteSub === 'enabled'
) {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
} }
if (sub.mergeSources === 'localFirst') { if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content); raw.unshift(sub.content);
@ -319,16 +363,24 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub; collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
} }
if (
!collectionIgnoreFailedRemoteSub && if (Object.keys(errors).length > 0) {
Object.keys(errors).length > 0 if (!collectionIgnoreFailedRemoteSub) {
) {
throw new Error( throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join( `组合订阅 ${collection.name} 的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (collectionIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理组合订阅失败`,
`${collection.name}`,
`子订阅 ${Object.keys(errors).join(
', ', ', ',
)} 发生错误, 请查看日志`, )} 发生错误, 请查看日志`,
); );
} }
}
// merge proxies with the original order // merge proxies with the original order
let proxies = Array.prototype.concat.apply( let proxies = Array.prototype.concat.apply(
@ -402,8 +454,12 @@ async function produceArtifact({
const allFiles = $.read(FILES_KEY); const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name); const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`); if (!file) throw new Error(`找不到文件 ${name}`);
let raw; let raw = '';
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) { if (file.type !== 'mihomoProfile') {
if (
content &&
!['localFirst', 'remoteFirst'].includes(mergeSources)
) {
raw = content; raw = content;
} else if (url) { } else if (url) {
const errors = {}; const errors = {};
@ -439,11 +495,14 @@ async function produceArtifact({
) { ) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile; fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
} }
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) { if (
!fileIgnoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error( throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join( `文件 ${file.name} 的远程文件 ${Object.keys(
', ', errors,
)} 发生错误, 请查看日志`, ).join(', ')} 发生错误, 请查看日志`,
); );
} }
if (mergeSources === 'localFirst') { if (mergeSources === 'localFirst') {
@ -490,19 +549,34 @@ async function produceArtifact({
) { ) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile; fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
} }
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
if (Object.keys(errors).length > 0) {
if (!fileIgnoreFailedRemoteFile) {
throw new Error( throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join( `文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (fileIgnoreFailedRemoteFile === 'enabled') {
$.notify(
`🌍 Sub-Store 处理文件失败`,
`${file.name}`,
`远程文件 ${Object.keys(errors).join(
', ', ', ',
)} 发生错误, 请查看日志`, )} 发生错误, 请查看日志`,
); );
} }
}
if (file.mergeSources === 'localFirst') { if (file.mergeSources === 'localFirst') {
raw.unshift(file.content); raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') { } else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content); raw.push(file.content);
} }
} }
}
if (produceType === 'raw') {
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
}
const files = (Array.isArray(raw) ? raw : [raw]).flat(); const files = (Array.isArray(raw) ? raw : [raw]).flat();
let filesContent = files let filesContent = files
.filter((i) => i != null && i !== '') .filter((i) => i != null && i !== '')
@ -512,12 +586,17 @@ async function produceArtifact({
const processed = const processed =
Array.isArray(file.process) && file.process.length > 0 Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process( ? await ProxyUtils.process(
{ $files: files, $content: filesContent, $options }, {
$files: files,
$content: filesContent,
$options,
$file: file,
},
file.process, file.process,
) )
: { $content: filesContent, $files: files, $options }; : { $content: filesContent, $files: files, $options };
return processed?.$content ?? ''; return (all ? processed : processed?.$content) ?? '';
} }
} }
@ -527,12 +606,15 @@ async function syncArtifacts() {
const files = {}; const files = {};
try { try {
const valid = [];
const invalid = []; const invalid = [];
const allSubs = $.read(SUBS_KEY); const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY); const allCols = $.read(COLLECTIONS_KEY);
const subNames = []; const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => { allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) { if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') { if (artifact.type === 'subscription') {
const subName = artifact.source; const subName = artifact.source;
const sub = findByName(allSubs, subName); const sub = findByName(allSubs, subName);
@ -553,6 +635,13 @@ async function syncArtifacts() {
} }
}); });
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) { if (subNames.length > 0) {
await Promise.all( await Promise.all(
subNames.map(async (subName) => { subNames.map(async (subName) => {
@ -601,27 +690,47 @@ async function syncArtifacts() {
files[encodeURIComponent(artifact.name)] = { files[encodeURIComponent(artifact.name)] = {
content: output, content: output,
}; };
valid.push(artifact.name);
} }
} catch (e) { } catch (e) {
$.error( $.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`, `生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
); );
invalid.push(artifact.name); invalid.push(artifact.name);
} }
}), }),
); );
if (invalid.length > 0) { $.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
if (valid.length === 0) {
throw new Error( throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`, `同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
); );
} }
const resp = await syncToGist(files); const resp = await syncToGist(files);
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) { for (const artifact of allArtifacts) {
if (artifact.sync) { if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
// extract real url from gist // extract real url from gist
let files = body.files; let files = body.files;
@ -649,9 +758,17 @@ async function syncArtifacts() {
} }
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!'); $.info('上传配置成功');
if (invalid.length > 0) {
throw new Error(
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.info(`同步配置成功 ${valid.length}`);
}
} catch (e) { } catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`); $.error(`同步配置失败,原因:${e.message ?? e}`);
throw e; throw e;
} }
} }
@ -661,7 +778,7 @@ async function syncAllArtifacts(_, res) {
await syncArtifacts(); await syncArtifacts();
success(res); success(res);
} catch (e) { } catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`); $.error(`同步配置失败,原因:${e.message ?? e}`);
failed( failed(
res, res,
new InternalServerError( new InternalServerError(
@ -738,6 +855,16 @@ async function syncArtifact(req, res) {
}); });
artifact.updated = new Date().getTime(); artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body); const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
let files = body.files; let files = body.files;
let isGitLab; let isGitLab;
if (Array.isArray(files)) { if (Array.isArray(files)) {

View File

@ -1,4 +1,4 @@
import { SETTINGS_KEY } from '@/constants'; import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils'; import { getPolicyDescriptor } from '@/utils';
@ -11,6 +11,8 @@ import {
validCheck, validCheck,
} from '@/utils/flow'; } from '@/utils/flow';
import $ from '@/core/app'; import $ from '@/core/app';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors'; import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
const clashPreprocessor = PROXY_PREPROCESSORS.find( const clashPreprocessor = PROXY_PREPROCESSORS.find(
(processor) => processor.name === 'Clash Pre-processor', (processor) => processor.name === 'Clash Pre-processor',
@ -130,22 +132,53 @@ export default async function download(
} }
} }
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/); const downloadUrlMatch = url
// if (downloadUrlMatch) { .split('#')[0]
// let type = downloadUrlMatch?.[1]; .match(/^\/api\/(file|module)\/(.+)/);
// let name = downloadUrlMatch?.[2]; if (downloadUrlMatch) {
// if (name == null) { let type = '';
// throw new Error(`本地 ${type} URL 无效: ${url}`); try {
// } type = downloadUrlMatch?.[1];
// name = decodeURIComponent(name); let name = downloadUrlMatch?.[2];
// const key = type === 'module' ? MODULES_KEY : FILES_KEY; if (name == null) {
// const item = findByName($.read(key), name); throw new Error(`本地 ${type} URL 无效: ${url}`);
// if (!item) { }
// throw new Error(`找不到本地 ${type}: ${name}`); name = decodeURIComponent(name);
// } const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到 ${type}: ${name}`);
}
// return item.content; if (type === 'module') {
// } return item.content;
} else {
return await produceArtifact({
type: 'file',
name,
});
}
} catch (err) {
$.error(
`Error when loading ${type}: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
return fs.readFileSync(url.split('#')[0], 'utf8');
} catch (err) {
$.error(
`Error when reading local file: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取文本内容: ${url}`);
}
}
if (!isNode && tasks.has(id)) { if (!isNode && tasks.has(id)) {
return tasks.get(id); return tasks.get(id);
@ -207,7 +240,7 @@ export default async function download(
if (preprocess) { if (preprocess) {
try { try {
if (clashPreprocessor.test(body)) { if (clashPreprocessor.test(body)) {
body = clashPreprocessor.parse(body); body = clashPreprocessor.parse(body, true);
} }
} catch (e) { } catch (e) {
$.error(`Clash Pre-processor error: ${e}`); $.error(`Clash Pre-processor error: ${e}`);
@ -273,3 +306,25 @@ export default async function download(
} }
return result; return result;
} }
export async function downloadFile(url, file) {
const undici = eval("require('undici')");
const fs = eval("require('fs')");
const { pipeline } = eval("require('stream/promises')");
const { Agent, interceptors, request } = undici;
$.info(`Downloading file...\nURL: ${url}\nFile: ${file}`);
const { body, statusCode } = await request(url, {
dispatcher: new Agent().compose(
interceptors.redirect({
maxRedirections: 3,
throwOnRedirect: true,
}),
),
});
if (statusCode !== 200)
throw new Error(`Failed to download file from ${url}`);
const fileStream = fs.createWriteStream(file);
await pipeline(body, fileStream);
$.info(`File downloaded from ${url} to ${file}`);
return file;
}

View File

@ -49,7 +49,7 @@ export async function getFlowHeaders(
} }
} }
} }
if ($arguments?.noFlow) { if ($arguments?.noFlow || !/^https?/.test(url)) {
return; return;
} }
const { isStash, isLoon, isShadowRocket, isQX } = ENV(); const { isStash, isLoon, isShadowRocket, isQX } = ENV();
@ -313,3 +313,41 @@ export function getRmainingDays(opt = {}) {
$.error(`getRmainingDays failed: ${e.message ?? e}`); $.error(`getRmainingDays failed: ${e.message ?? e}`);
} }
} }
export function normalizeFlowHeader(flowHeaders) {
try {
// 使用 Map 保持顺序并处理重复键
const kvMap = new Map();
flowHeaders
.split(';')
.map((p) => p.trim())
.filter(Boolean)
.forEach((pair) => {
const eqIndex = pair.indexOf('=');
if (eqIndex === -1) return;
const key = pair.slice(0, eqIndex).trim();
const encodedValue = pair.slice(eqIndex + 1).trim();
// 只保留第一个出现的 key
if (!kvMap.has(key)) {
try {
// 解码 URI 组件并保留原始值作为 fallback
const decodedValue = decodeURIComponent(encodedValue);
kvMap.set(key, decodedValue);
} catch (e) {
kvMap.set(key, encodedValue);
}
}
});
// 拼接标准化字符串
return Array.from(kvMap.entries())
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
.join('; ');
} catch (e) {
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
return flowHeaders;
}
}

View File

@ -18,7 +18,9 @@ const ISOFlags = {
'🇧🇬': ['BG', 'BGR'], '🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'], '🇧🇭': ['BH', 'BHR'],
'🇧🇴': ['BO', 'BOL'], '🇧🇴': ['BO', 'BOL'],
'🇧🇳': ['BN', 'BRN'],
'🇧🇷': ['BR', 'BRA'], '🇧🇷': ['BR', 'BRA'],
'🇧🇹': ['BT', 'BTN'],
'🇧🇾': ['BY', 'BLR'], '🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'], '🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'], '🇨🇭': ['CH', 'CHE'],
@ -40,6 +42,7 @@ const ISOFlags = {
'🇬🇪': ['GE', 'GEO'], '🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'], '🇬🇷': ['GR', 'GRC'],
'🇬🇹': ['GT', 'GTM'], '🇬🇹': ['GT', 'GTM'],
'🇬🇺': ['GU', 'GUM'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'], '🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'], '🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'], '🇭🇺': ['HU', 'HUN'],
@ -59,12 +62,15 @@ const ISOFlags = {
'🇮🇷': ['IR', 'IRN'], '🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'], '🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'], '🇮🇹': ['IT', 'ITA'],
'🇱🇦': ['LA', 'LAO'],
'🇱🇰': ['LK', 'LKA'],
'🇱🇹': ['LT', 'LTU'], '🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'], '🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'], '🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'], '🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'], '🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'], '🇳🇬': ['NG', 'NGA'],
'🇲🇲': ['MM', 'MMR'],
'🇲🇰': ['MK', 'MKD'], '🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'], '🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'], '🇲🇴': ['MO', 'MAC', 'CTM'],
@ -83,6 +89,7 @@ const ISOFlags = {
'🇵🇷': ['PR', 'PRI'], '🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'], '🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'], '🇵🇾': ['PY', 'PRY'],
'🇵🇬': ['PG', 'PNG'],
'🇷🇴': ['RO', 'ROU'], '🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'], '🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'], '🇷🇪': ['RE', 'REU'],
@ -142,8 +149,10 @@ export function getFlag(name) {
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'], '🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
'🇧🇭': ['Bahrain', '巴林'], '🇧🇭': ['Bahrain', '巴林'],
'🇧🇷': ['Brazil', '巴西', '圣保罗'], '🇧🇷': ['Brazil', '巴西', '圣保罗'],
'🇧🇳': ['Brunei', '文莱', '汶萊'],
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'], '🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
'🇧🇴': ['Bolivia', '玻利维亚'], '🇧🇴': ['Bolivia', '玻利维亚'],
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
'🇨🇦': [ '🇨🇦': [
'Canada', 'Canada',
'加拿大', '加拿大',
@ -194,6 +203,7 @@ export function getFlag(name) {
], ],
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'], '🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
'🇬🇷': ['Greece', '希腊', '希臘'], '🇬🇷': ['Greece', '希腊', '希臘'],
'🇬🇺': ['Guam', '关岛', '關島'],
'🇬🇹': ['Guatemala', '危地马拉'], '🇬🇹': ['Guatemala', '危地马拉'],
'🇭🇰': [ '🇭🇰': [
'Hongkong', 'Hongkong',
@ -254,11 +264,14 @@ export function getFlag(name) {
'🇮🇷': ['Iran', '伊朗'], '🇮🇷': ['Iran', '伊朗'],
'🇮🇸': ['Iceland', '冰岛', '冰島'], '🇮🇸': ['Iceland', '冰岛', '冰島'],
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'], '🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
'🇱🇦': ['Laos', '老挝', '老撾'],
'🇱🇹': ['Lithuania', '立陶宛'], '🇱🇹': ['Lithuania', '立陶宛'],
'🇱🇺': ['Luxembourg', '卢森堡'], '🇱🇺': ['Luxembourg', '卢森堡'],
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'], '🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
'🇲🇦': ['Morocco', '摩洛哥'], '🇲🇦': ['Morocco', '摩洛哥'],
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'], '🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'], '🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'], '🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
'🇲🇳': ['Mongolia', '蒙古'], '🇲🇳': ['Mongolia', '蒙古'],
@ -284,6 +297,7 @@ export function getFlag(name) {
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'], '🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
'🇵🇷': ['Puerto Rico', '波多黎各'], '🇵🇷': ['Puerto Rico', '波多黎各'],
'🇵🇹': ['Portugal', '葡萄牙'], '🇵🇹': ['Portugal', '葡萄牙'],
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
'🇵🇾': ['Paraguay', '巴拉圭'], '🇵🇾': ['Paraguay', '巴拉圭'],
'🇷🇴': ['Romania', '罗马尼亚'], '🇷🇴': ['Romania', '罗马尼亚'],
'🇷🇸': ['Serbia', '塞尔维亚'], '🇷🇸': ['Serbia', '塞尔维亚'],

View File

@ -114,8 +114,11 @@ export default class Gist {
return; return;
}); });
} else { } else {
return this.http.get('/gists').then((response) => { return this.http
.get('/gists?per_page=100&page=1')
.then((response) => {
const gists = JSON.parse(response.body); const gists = JSON.parse(response.body);
$.info(`获取到当前 GitHub 用户的 gist: ${gists.length}`);
for (let g of gists) { for (let g of gists) {
if (g.description === this.key) { if (g.description === this.key) {
return g; return g;
@ -277,7 +280,7 @@ export default class Gist {
return Promise.reject(err); return Promise.reject(err);
} }
} else { } else {
return Promise.reject('找不到 Sub-Store Gist'); return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);
} }
} }
} }

View File

@ -117,7 +117,42 @@ function numberToString(value) {
: BigInt(value).toString(); : BigInt(value).toString();
} }
function isValidUUID(uuid) {
return (
typeof uuid === 'string' &&
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
uuid,
)
);
}
function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) {
return '';
}
const pad = (num) => String(num).padStart(2, '0');
const replacements = {
YYYY: d.getFullYear(),
MM: pad(d.getMonth() + 1),
DD: pad(d.getDate()),
HH: pad(d.getHours()),
mm: pad(d.getMinutes()),
ss: pad(d.getSeconds()),
};
return format.replace(
/YYYY|MM|DD|HH|mm|ss/g,
(match) => replacements[match],
);
}
export { export {
formatDateTime,
isValidUUID,
ipAddress, ipAddress,
isIPv4, isIPv4,
isIPv6, isIPv6,

View File

@ -4,6 +4,8 @@ import {
SCHEMA_VERSION_KEY, SCHEMA_VERSION_KEY,
ARTIFACTS_KEY, ARTIFACTS_KEY,
RULES_KEY, RULES_KEY,
FILES_KEY,
TOKENS_KEY,
} from '@/constants'; } from '@/constants';
import $ from '@/core/app'; import $ from '@/core/app';
@ -55,7 +57,17 @@ function doMigrationV2() {
const newRules = Object.values(rules); const newRules = Object.values(rules);
$.write(newRules, RULES_KEY); $.write(newRules, RULES_KEY);
// 5. delete builtin rules // 5. migrate files
const files = $.read(FILES_KEY) || {};
const newFiles = Object.values(files);
$.write(newFiles, FILES_KEY);
// 6. migrate tokens
const tokens = $.read(TOKENS_KEY) || {};
const newTokens = Object.values(tokens);
$.write(newTokens, TOKENS_KEY);
// 7. delete builtin rules
delete $.cache.builtin; delete $.cache.builtin;
$.info('Migration complete!'); $.info('Migration complete!');

View File

@ -24,7 +24,7 @@ class ResourceCache {
this._cleanup(); this._cleanup();
} }
_cleanup() { _cleanup(prefix, expires) {
// clear obsolete cached resource // clear obsolete cached resource
let clear = false; let clear = false;
Object.entries(this.resourceCache).forEach((entry) => { Object.entries(this.resourceCache).forEach((entry) => {
@ -35,7 +35,11 @@ class ResourceCache {
$.delete(`#${id}`); $.delete(`#${id}`);
clear = true; clear = true;
} }
if (new Date().getTime() - updated.time > this.expires) { if (
new Date().getTime() - updated.time >
(expires ?? this.expires) ||
(prefix && id.startsWith(prefix))
) {
delete this.resourceCache[id]; delete this.resourceCache[id];
clear = true; clear = true;
} }
@ -52,10 +56,15 @@ class ResourceCache {
$.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY); $.write(JSON.stringify(this.resourceCache), SCRIPT_RESOURCE_CACHE_KEY);
} }
get(id) { get(id, expires, remove) {
const updated = this.resourceCache[id] && this.resourceCache[id].time; const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) { if (updated) {
if (new Date().getTime() - updated <= (expires ?? this.expires))
return this.resourceCache[id].data; return this.resourceCache[id].data;
if (remove) {
delete this.resourceCache[id];
this._persist();
}
} }
return null; return null;
} }

View File

@ -1,3 +1,7 @@
import gte from 'semver/functions/gte';
import coerce from 'semver/functions/coerce';
import $ from '@/core/app';
export function getUserAgentFromHeaders(headers) { export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers); const keys = Object.keys(headers);
let UA = ''; let UA = '';
@ -43,7 +47,7 @@ export function getPlatformFromUserAgent({ ua, UA, accept }) {
return 'Clash'; return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) { } else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray'; return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1) { } else if (ua.indexOf('sing-box') !== -1 || ua.indexOf('singbox') !== -1) {
return 'sing-box'; return 'sing-box';
} else if (accept.indexOf('application/json') === 0) { } else if (accept.indexOf('application/json') === 0) {
return 'JSON'; return 'JSON';
@ -56,3 +60,42 @@ export function getPlatformFromHeaders(headers) {
const { UA, ua, accept } = getUserAgentFromHeaders(headers); const { UA, ua, accept } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA, accept }); return getPlatformFromUserAgent({ ua, UA, accept });
} }
export function shouldIncludeUnsupportedProxy(platform, ua) {
// try {
// const target = getPlatformFromUserAgent({
// UA: ua,
// ua: ua.toLowerCase(),
// });
// if (!['Stash', 'Egern', 'Loon'].includes(target)) {
// return false;
// }
// const coerceVersion = coerce(ua);
// $.log(JSON.stringify(coerceVersion, null, 2));
// const { version } = coerceVersion;
// if (
// platform === 'Stash' &&
// target === 'Stash' &&
// gte(version, '3.1.0')
// ) {
// return true;
// }
// if (
// platform === 'Egern' &&
// target === 'Egern' &&
// gte(version, '1.29.0')
// ) {
// return true;
// }
// // Loon 的 UA 不规范, version 取出来是 build
// if (
// platform === 'Loon' &&
// target === 'Loon' &&
// gte(version, '842.0.0')
// ) {
// return true;
// }
// } catch (e) {
// $.error(`获取版本号失败: ${e}`);
// }
return false;
}

View File

@ -34,4 +34,6 @@ export default {
load, load,
safeDump, safeDump,
dump, dump,
parse: safeLoad,
stringify: safeDump,
}; };

View File

@ -17,7 +17,14 @@ export default function express({ substore: $, port, host }) {
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, limit: '1mb' })); const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb';
$.info(`[BACKEND] body JSON limit: ${limit}`);
app.use(
bodyParser.json({
verify: rawBodySaver,
limit,
}),
);
app.use( app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }), bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
); );
@ -31,7 +38,7 @@ export default function express({ substore: $, port, host }) {
app.start = () => { app.start = () => {
const listener = app.listen(port, host, () => { const listener = app.listen(port, host, () => {
const { address, port } = listener.address(); const { address, port } = listener.address();
$.info(`[BACKEND] ${address}:${port}`); $.info(`[BACKEND] listening on ${address}:${port}`);
}); });
}; };
return app; return app;

View File

@ -10,6 +10,14 @@ const isEgern = 'object' == typeof egern;
const isLanceX = 'undefined' != typeof $native; const isLanceX = 'undefined' != typeof $native;
const isGUIforCores = typeof $Plugins !== 'undefined'; const isGUIforCores = typeof $Plugins !== 'undefined';
function isPlainObject(obj) {
return (
obj !== null &&
typeof obj === 'object' &&
[null, Object.prototype].includes(Object.getPrototypeOf(obj))
);
}
export class OpenAPI { export class OpenAPI {
constructor(name = 'untitled', debug = false) { constructor(name = 'untitled', debug = false) {
this.name = name; this.name = name;
@ -18,6 +26,10 @@ export class OpenAPI {
this.http = HTTP(); this.http = HTTP();
this.env = ENV(); this.env = ENV();
if (isNode) {
const dotenv = eval(`require("dotenv")`);
dotenv.config();
}
this.node = (() => { this.node = (() => {
if (isNode) { if (isNode) {
const fs = eval("require('fs')"); const fs = eval("require('fs')");
@ -58,29 +70,50 @@ export class OpenAPI {
const basePath = const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.'; eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
let rootPath = `${basePath}/root.json`; let rootPath = `${basePath}/root.json`;
const backupRootPath = `${basePath}/root_${Date.now()}.json`;
this.log(`Root path: ${rootPath}`); this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) { if (this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), { try {
flag: 'wx',
});
this.root = {};
} else {
this.root = JSON.parse( this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`), this.node.fs.readFileSync(`${rootPath}`),
); );
} catch (e) {
this.node.fs.copyFileSync(rootPath, backupRootPath);
this.error(
`Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`,
);
}
}
if (!isPlainObject(this.root)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'w',
});
this.root = {};
} }
// create a json file with the given name if not exists // create a json file with the given name if not exists
let fpath = `${basePath}/${this.name}.json`; let fpath = `${basePath}/${this.name}.json`;
const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
this.log(`Data path: ${fpath}`); this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) { if (this.node.fs.existsSync(fpath)) {
try {
this.cache = JSON.parse(
this.node.fs.readFileSync(`${fpath}`),
);
} catch (e) {
this.node.fs.copyFileSync(fpath, backupPath);
this.error(
`Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`,
);
}
}
if (!isPlainObject(this.cache)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx', flag: 'w',
}); });
this.cache = {}; this.cache = {};
} else {
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
} }
} }
} }
@ -337,16 +370,17 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
opts: options.opts, opts: options.opts,
}); });
} else if (isLoon || isSurge || isNode) { } else if (isLoon || isSurge || isNode) {
worker = new Promise((resolve, reject) => { worker = new Promise(async (resolve, reject) => {
const request = isNode
? eval("require('request')")
: $httpClient;
const body = options.body; const body = options.body;
const opts = JSON.parse(JSON.stringify(options)); const opts = JSON.parse(JSON.stringify(options));
opts.body = body; opts.body = body;
opts.timeout = opts.timeout || 8000;
if (!isNode && opts.timeout) { if (opts.timeout) {
opts.timeout++; opts.timeout++;
if (isNaN(opts.timeout)) {
opts.timeout = 8000;
}
if (!isNode) {
let unit = 'ms'; let unit = 'ms';
// 这些客户端单位为 s // 这些客户端单位为 s
if (isSurge || isStash || isShadowRocket) { if (isSurge || isStash || isShadowRocket) {
@ -356,7 +390,69 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
// Loon 为 ms // Loon 为 ms
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`); // console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
} }
request[method.toLowerCase()](opts, (err, response, body) => { }
if (isNode) {
const undici = eval("require('undici')");
const {
ProxyAgent,
EnvHttpProxyAgent,
request,
interceptors,
} = undici;
const agentOpts = {
connect: {
rejectUnauthorized:
opts.strictSSL === false ||
opts.insecure === true
? false
: true,
},
bodyTimeout: opts.timeout,
headersTimeout: opts.timeout,
};
try {
const url = new URL(opts.url);
if (url.username || url.password) {
opts.headers = {
...(opts.headers || {}),
Authorization: `Basic ${Buffer.from(
`${url.username || ''}:${
url.password || ''
}`,
).toString('base64')}`,
};
}
const response = await request(opts.url, {
...opts,
method: method.toUpperCase(),
dispatcher: (opts.proxy
? new ProxyAgent({
...agentOpts,
uri: opts.proxy,
})
: new EnvHttpProxyAgent(agentOpts)
).compose(
interceptors.redirect({
maxRedirections: 3,
throwOnMaxRedirects: true,
}),
),
});
resolve({
statusCode: response.statusCode,
headers: response.headers,
body:
opts.encoding === null
? await response.body.arrayBuffer()
: await response.body.text(),
});
} catch (e) {
reject(e);
}
} else {
$httpClient[method.toLowerCase()](
opts,
(err, response, body) => {
// if (err) { // if (err) {
// console.log(err); // console.log(err);
// } else { // } else {
@ -371,11 +467,14 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
if (err) reject(err); if (err) reject(err);
else else
resolve({ resolve({
statusCode: response.status || response.statusCode, statusCode:
response.status || response.statusCode,
headers: response.headers, headers: response.headers,
body, body,
}); });
}); },
);
}
}); });
} else if (isGUIforCores) { } else if (isGUIforCores) {
worker = new Promise(async (resolve, reject) => { worker = new Promise(async (resolve, reject) => {

View File

View File

@ -13,7 +13,13 @@ function operator(proxies = [], targetPlatform, context) {
// 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名 // 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名 // 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
// 7. `tls-fingerprint` 为 tls 指纹 // 7. `tls-fingerprint` 为 tls 指纹
// 8. `underlying-proxy` 为前置代理 // 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
// 例如 $server['underlying-proxy'] = '名称'
// 只给 mihomo 输出的话, `dialer-proxy` 也行
// 只给 sing-box 输出的话, `detour` 也行
// 只给 Egern 输出的话, `prev_hop` 也行
// 只给 Shadowrocket 输出的话, `chain` 也行
// 输出到 Clash/Stash 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除 // 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
// 10. `sni` 在某些协议里会自动与 `servername` 转换 // 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512) // 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
@ -21,8 +27,14 @@ function operator(proxies = [], targetPlatform, context) {
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时 // 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔 // 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值 // 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块 // require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
// 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件
// const fs = eval(`require("fs")`)
// // const path = eval(`require("path")`)
// fs.writeFileSync('/tmp/1.txt', $content, "utf8");
// $arguments 为传入的脚本参数 // $arguments 为传入的脚本参数
@ -35,8 +47,27 @@ function operator(proxies = [], targetPlatform, context) {
// 先这样处理 encodeURIComponent('arg1=a&arg2=b') // 先这样处理 encodeURIComponent('arg1=a&arg2=b')
// /api/file/foo?$options=arg1%3Da%26arg2%3Db // /api/file/foo?$options=arg1%3Da%26arg2%3Db
// 默认会带上 _req 字段, 结构为
// {
// method,
// url,
// path,
// query,
// params,
// headers,
// body,
// }
// console.log($options) // console.log($options)
// 若设置 $options._res.headers
// 则会在输出文件时设置响应头, 例如:
// $options._res = {
// headers: {
// 'X-Custom': '1'
// }
// }
// targetPlatform 为输出的目标平台 // targetPlatform 为输出的目标平台
// lodash // lodash
@ -47,8 +78,38 @@ function operator(proxies = [], targetPlatform, context) {
// scriptResourceCache 缓存 // scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003 // 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache // const cache = scriptResourceCache
// cache.set(id, data) // 设置
// cache.get(id) // cache.set('a:1', 1)
// cache.set('a:2', 2)
// 获取
// cache.get('a:1')
// 支持第二个参数: 自定义过期时间
// 支持第三个参数: 是否删除过期项
// cache.get('a:2', 1000, true)
// 清理
// cache._cleanup()
// 支持第一个参数: 匹配前缀的项也一起删除
// 支持第二个参数: 自定义过期时间
// cache._cleanup('a:', 1000)
// 关于缓存时长
// 拉取 Sub-Store 订阅时, 会自动拉取远程订阅
// 远程订阅缓存是 1 小时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true
// 当使用相关脚本时, 若在对应的脚本中使用参数开启缓存, 可设置持久化缓存 sub-store-csr-expiration-time 的值来自定义默认缓存时长, 默认为 172800000 (48 * 3600 * 1000, 即 48 小时)
// 🎈Loon 可在插件中设置
// 其他平台同理, 持久化缓存数据在 JSON 里
// 当配合脚本使用时, 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活
// async function operator() {
// scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
// }
// ProxyUtils 为节点处理工具 // ProxyUtils 为节点处理工具
// 可参考 https://t.me/zhetengsha/1066 // 可参考 https://t.me/zhetengsha/1066
@ -67,8 +128,13 @@ function operator(proxies = [], targetPlatform, context) {
// getISO, // 获取 ISO 3166-1 alpha-2 代码 // getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类 // Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js // download, // 内部的下载方法, 见 backend/src/utils/download.js
// downloadFile, // 下载二进制文件, 见 backend/src/utils/download.js
// MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269 // MMDB, // Node.js 环境 可用于模拟 Surge/Loon 的 $utils.ipasn, $utils.ipaso, $utils.geoip. 具体见 https://t.me/zhetengsha/1269
// isValidUUID, // 辅助判断是否为有效的 UUID
// Buffer, // https://github.com/feross/buffer
// Base64, // https://github.com/dankogai/js-base64
// } // }
// 为兼容 https://github.com/xishang0128/sparkle 的 JavaScript 覆写, 也可以直接使用 `b64d`(Base64 解码), `b64e`(Base64 编码), `Buffer`, `yaml`(简单兼容了下 `yaml.parse` 和 `yaml.stringify`)
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009 // 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
// ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一 // ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB