Compare commits

...

458 Commits

Author SHA1 Message Date
xream
8a417d9852 feat: sub-store.json 初始化时, 支持读取 Base64 内容 2025-07-19 13:27:25 +08:00
xream
ebff520499 feat: 手动还原支持 Base64 文本文件
Some checks failed
build / build (push) Has been cancelled
2025-07-19 12:55:32 +08:00
xream
876d2e92ca feat: 处理 clash 系和 sing-box 的 Early Data 2025-07-19 06:47:24 +08:00
xream
63064bc596 feat: Gist 备份默认为 Base64 编码方式
Some checks failed
build / build (push) Has been cancelled
2025-07-18 17:21:11 +08:00
xream
e81245a5bb feat: Gist 备份恢复增加 Base64 编码方式 2025-07-18 17:12:51 +08:00
xream
217fdae7f1 feat: Node 环境使用 SUB_STORE_DATA_URL 恢复备份后, 支持 SUB_STORE_DATA_URL_POST 执行自定义命令 2025-07-18 16:10:26 +08:00
xream
ef4d0a228b feat: 支持从 Gist 恢复时保留当前 GitHub Token 2025-07-18 14:54:13 +08:00
xream
e816e5b3c0 fix: 尝试修复上传 Gist 2025-07-18 13:37:35 +08:00
xream
005051c4ac feat: Node 运行环境支持 SOCKS5 代理 2025-07-18 13:13:43 +08:00
xream
61078b10f3 feat: 备份数据到 Gist 时, 清除 GitHub Token. 恢复后请重新设置 GitHub Token
Some checks failed
build / build (push) Has been cancelled
2025-07-18 10:14:18 +08:00
xream
bfa1b11a0e feat: 正式弃用 SUB_STORE_BACKEND_CRON 和 SUB_STORE_CRON, 请使用 SUB_STORE_BACKEND_SYNC_CRON
Some checks failed
build / build (push) Has been cancelled
2025-07-17 18:49:34 +08:00
xream
7d60aed50d feat: 优化 Surge ws 传输层引号解析
Some checks failed
build / build (push) Has been cancelled
2025-07-12 21:06:02 +08:00
xream
e20d0c1dc9 feat: 订阅流量信息中的数值取整以兼容部分客户端解析; 不包含有效节点的订阅不写入乐观缓存
Some checks failed
build / build (push) Has been cancelled
2025-07-12 11:11:40 +08:00
xream
c5660024fb doc: demo.js 2025-07-12 09:26:04 +08:00
xream
76e12bd6a0 feat: Surge username password 逻辑优化
Some checks failed
build / build (push) Has been cancelled
2025-07-11 10:36:43 +08:00
xream
3a33446422 doc: demo.js
Some checks failed
build / build (push) Has been cancelled
2025-07-10 12:37:19 +08:00
xream
17b12711b4 fix: 修复 sing-box 和 mihomo 的 ip-version 2025-07-10 12:32:11 +08:00
xream
c266635ba1 fix: 修复 sing-box shadowsocks+shadow-tls
Some checks failed
build / build (push) Has been cancelled
2025-07-10 08:00:39 +08:00
xream
f34eac9568 feat: 节点本地内容支持 JSON5 2025-07-10 01:37:16 +08:00
xream
aa5b51a3cc feat: 放宽 sing-box VLESS flow 逻辑 2025-07-10 01:28:15 +08:00
xream
b8897dd94a fix: 修复 Egern transport 兼容性
Some checks failed
build / build (push) Has been cancelled
2025-07-08 21:59:16 +08:00
xream
71958e6bb1 doc: demo.js 2025-07-08 09:43:31 +08:00
xream
fa5f88ae85 fix: 修复 snell 版本过滤范围
Some checks failed
build / build (push) Has been cancelled
2025-07-01 20:56:55 +08:00
xream
212aa7730d fix: 修复阿里 httpdns edns
Some checks failed
build / build (push) Has been cancelled
2025-06-25 19:36:08 +08:00
xream
4c5c9baa3e release: bump version
Some checks failed
build / build (push) Has been cancelled
2025-06-23 21:37:50 +08:00
xream
25dcbdc4dd Merge pull request #460 from Ayideyia/master
适配下游客户端API
2025-06-23 21:37:24 +08:00
啊伊的伊阿
282780b791 适配下游客户端API 2025-06-23 21:33:49 +08:00
xream
cde09541cf feat: anytls 支持 min-idle-session
Some checks failed
build / build (push) Has been cancelled
2025-06-19 10:38:20 +08:00
xream
6731c42edb doc: README 2025-06-14 12:02:32 +08:00
xream
64b9505035 feat: token 唯一性检测增加 type 和 name 2025-06-09 18:42:35 +08:00
xream
b0347637bc feat: SOCKS5 解析去除密码首尾双引号 2025-06-09 12:37:32 +08:00
xream
ab67ce9f5a feat: ProxyUtils 新增 JSON5 2025-06-05 11:01:41 +08:00
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, 拉取文件时将设置自定义响应头
Some checks failed
build / build (push) Has been cancelled
2025-05-28 13:46:57 +08:00
xream
532be2ff8c Stash 正式版支持 VLESS REALITY(xtls-rprx-vision)
Some checks failed
build / build (push) Has been cancelled
2025-05-27 19:46:31 +08:00
xream
37fc7ac88e feat: VMess 支持 kcp/quic(正确处理 type, host, path, fp, alpn, tls等参数)
Some checks failed
build / build (push) Has been cancelled
2025-05-27 03:01:28 +08:00
xream
9e0028219d feat: Shadowrocket 支持 anytls
Some checks failed
build / build (push) Has been cancelled
2025-05-26 17:24:39 +08:00
xream
54750d552b feat: 为 env 响应增加如何使用前端搭配后端的引导说明
Some checks failed
build / build (push) Has been cancelled
2025-05-25 00:58:06 +08:00
xream
0e7561a069 feat: Node.js 环境中 JSON 数据文件校验失败后会备份原文件, 创建新文件
Some checks failed
build / build (push) Has been cancelled
2025-05-24 18:40:30 +08:00
xream
6804c6368a fix: 修复 QX VLESS TLS
Some checks failed
build / build (push) Has been cancelled
2025-05-23 22:36:08 +08:00
xream
9c5d6e9a10 feat: 单条订阅和文件支持链接参数 produceType raw, 此时返回原始数据的数组
Some checks failed
build / build (push) Has been cancelled
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
Some checks failed
build / build (push) Has been cancelled
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 说明
Some checks failed
build / build (push) Has been cancelled
2025-05-18 17:21:54 +08:00
xream
18659d1cc8 feat: Node.js 环境下 API / 路由不自动跳转到 sub-store.vercel.app
Some checks failed
build / build (push) Has been cancelled
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
Some checks failed
build / build (push) Has been cancelled
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
Some checks failed
build / build (push) Has been cancelled
2025-05-11 16:21:28 +08:00
xream
d2c3956884 feat: QX 正式支持 SS2022
Some checks failed
build / build (push) Has been cancelled
2025-05-07 02:59:19 +08:00
xream
21c1e11976 feat: 兼容非标 Shadowsocks URI 输入
Some checks failed
build / build (push) Has been cancelled
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)
Some checks failed
build / build (push) Has been cancelled
2025-04-27 09:45:14 +08:00
xream
da9b1d8795 feat: 输出到 Clash/Stash/Shadowrocket 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能
Some checks failed
build / build (push) Has been cancelled
2025-04-26 13:46:58 +08:00
xream
4c4bda563a feat: Stash 输出中过滤掉有前置代理的节点, 并在日志中提示
Some checks failed
build / build (push) Has been cancelled
2025-04-22 09:42:32 +08:00
xream
95f181351a feat: 忽略失败的远程选择支持开启通知(前端 >= 2.15.17)
Some checks failed
build / build (push) Has been cancelled
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 解析增加默认节点名
Some checks failed
build / build (push) Has been cancelled
2025-04-20 20:10:08 +08:00
xream
55cc7dcd16 fix: 修复 URI 输出
Some checks failed
build / build (push) Has been cancelled
2025-04-19 16:44:40 +08:00
xream
4f745b0232 feat: sing-box 输出支持 brutal
Some checks failed
build / build (push) Has been cancelled
2025-04-18 22:49:19 +08:00
xream
28b233b62c fix: 修复 URI 输出
Some checks failed
build / build (push) Has been cancelled
2025-04-18 15:04:06 +08:00
xream
44d72523ce feat: AnyTLS URI 支持 UDP 参数
Some checks failed
build / build (push) Has been cancelled
2025-04-18 12:24:31 +08:00
xream
b60995f7ac feat: Loon 输入输出正式支持 VLESS XTLS/REALITY, VMess REALITY
Some checks failed
build / build (push) Has been cancelled
2025-04-17 09:57:56 +08:00
xream
a262dfbbe8 fix: 修复 Loon block-quic 参数
Some checks failed
build / build (push) Has been cancelled
2025-04-16 07:28:28 +08:00
xream
166f3cb447 feat: 支持 QX udp-over-tcp=true/sp.v1/sp.v2
Some checks failed
build / build (push) Has been cancelled
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
Some checks failed
build / build (push) Has been cancelled
2025-04-13 03:28:11 +08:00
xream
0d575e6e88 doc: demo.js
Some checks failed
build / build (push) Has been cancelled
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 字段, 包含请求信息
Some checks failed
build / build (push) Has been cancelled
2025-04-08 12:48:38 +08:00
xream
e054b71a62 feat: Shadowrocket VMess ws 传输层增加默认 path
Some checks failed
build / build (push) Has been cancelled
2025-04-03 22:33:31 +08:00
xream
7213cea16c feat: Stash 正式版支持 SS2022, 测试版(>=3.1.0) 支持 VLESS REALITY(xtls-rprx-vision)
Some checks are pending
build / build (push) Waiting to run
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 自动开启)
Some checks failed
build / build (push) Has been cancelled
2025-04-01 18:22:28 +08:00
xream
39829fa97a feat: QX 输入值支持 =
Some checks failed
build / build (push) Has been cancelled
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)
Some checks are pending
build / build (push) Waiting to run
2025-03-28 12:46:51 +08:00
xream
80955aa339 doc: 标记 Clash Deprecated 2025-03-27 19:53:46 +08:00
xream
4d27e5bdac feat: 脚本链接叠加参数调整
Some checks are pending
build / build (push) Waiting to run
2025-03-27 12:52:19 +08:00
xream
e2011de69e feat: Loon 解析器支持参数 resourceUrlOnly 仅使用远程资源, 忽略 Loon 自身解析数据
Some checks failed
build / build (push) Has been cancelled
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 判断
Some checks are pending
build / build (push) Waiting to run
2025-03-25 17:49:47 +08:00
xream
16a5995d21 fix: 修复 ss shadow-tls
Some checks failed
build / build (push) Has been cancelled
2025-03-23 14:32:24 +08:00
xream
15b55f6d1a feat: 更新文件时, 更新同步配置; 更新单条订阅/组合订阅时, 更新 mihomo 覆写
Some checks failed
build / build (push) Has been cancelled
2025-03-21 00:36:42 +08:00
xream
8e5ce26e7b fix: 修复重置后端数据后无默认字段的问题
Some checks are pending
build / build (push) Waiting to run
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
Some checks are pending
build / build (push) Waiting to run
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: 日志
Some checks failed
build / build (push) Has been cancelled
2025-03-18 13:34:50 +08:00
Aritro37
dd92a26e6c Perf: 提前加载 .env;后端复用前端 Path
Some checks are pending
build / build (push) Waiting to run
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: 增加更多的同步配置日志
Some checks are pending
build / build (push) Waiting to run
2025-03-16 15:48:47 +08:00
xream
a70dc7b913 feat: undici 配置重定向
Some checks are pending
build / build (push) Waiting to run
2025-03-15 22:50:47 +08:00
xream
fc56df7bfd fix: 处理 YAML short-idnull 的情况
Some checks are pending
build / build (push) Waiting to run
2025-03-15 16:29:19 +08:00
xream
1281df59f3 feat: 增强 VMess URI 解析兼容性; 修改导出文件名格式
Some checks failed
build / build (push) Has been cancelled
2025-03-13 20:19:45 +08:00
xream
1faa3fb793 fix: 修复 VMess URI IPv6 格式
Some checks are pending
build / build (push) Waiting to run
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 字段
Some checks failed
build / build (push) Has been cancelled
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 时自动开启)
Some checks failed
build / build (push) Has been cancelled
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
Some checks failed
build / build (push) Has been cancelled
2025-03-05 15:25:16 +08:00
xream
d255390d48 fix: 修复 Surge shadow-tls-password 引号解析
Some checks are pending
build / build (push) Waiting to run
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
Some checks are pending
build / build (push) Waiting to run
2025-03-03 20:52:31 +08:00
xream
c2c39c5de6 fix: 修复 Egern 输出
Some checks are pending
build / build (push) Waiting to run
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)
Some checks are pending
build / build (push) Waiting to run
2025-03-02 11:06:33 +08:00
xream
9426f128c4 feat: Surge 输出会判断 HTTP 是否 headers 字段
Some checks are pending
build / build (push) Waiting to run
2025-03-01 21:43:14 +08:00
xream
ebc7173c95 feat: 文件类型为 mihomo 配置时, 来源可以为无
Some checks are pending
build / build (push) Waiting to run
2025-03-01 08:45:17 +08:00
xream
dd4e0cef68 feat: 扩展 scriptResourceCache 缓存, 详见 demo.js
Some checks are pending
build / build (push) Waiting to run
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 覆写可以多次使用
Some checks are pending
build / build (push) Waiting to run
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 数量日志
Some checks are pending
build / build (push) Waiting to run
2025-02-26 18:50:11 +08:00
xream
d53947d820 feat: sing-box 支持 anytls
Some checks failed
build / build (push) Has been cancelled
2025-02-23 09:48:09 +08:00
xream
7e75031e92 fix: 修复 short-id 正则
Some checks are pending
build / build (push) Waiting to run
2025-02-22 14:25:06 +08:00
xream
4a07c02dc1 feat: 支持 Shadowrocket Shadowsocks 输入中的 Shadow TLS 参数
Some checks failed
build / build (push) Has been cancelled
2025-02-21 01:44:34 +08:00
xream
95d6688539 fix: 修复 Shadowrocket 输出的 Shadow TLS
Some checks are pending
build / build (push) Waiting to run
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
Some checks are pending
build / build (push) Waiting to run
2025-02-19 17:15:31 +08:00
xream
ffabcc9391 feat: 支持 anytls 协议
Some checks are pending
build / build (push) Waiting to run
2025-02-19 17:01:40 +08:00
xream
0825f15d04 feat: Egern 支持 Shadow TLS
Some checks failed
build / build (push) Has been cancelled
2025-02-18 15:07:24 +08:00
xream
fbf6b5ce6e fix: UUID
Some checks failed
build / build (push) Has been cancelled
2025-02-16 05:05:33 +08:00
xream
3eb0816c88 fix: 修复 TUIC URI
Some checks are pending
build / build (push) Waiting to run
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 字段
Some checks are pending
build / build (push) Waiting to run
2025-02-14 23:27:01 +08:00
xream
5058662651 feat: 下载文件名增加前后缀
Some checks are pending
build / build (push) Waiting to run
2025-02-14 15:39:13 +08:00
xream
f9d120bac3 feat: 兼容 v2rayN 非标 TUIC URI
Some checks failed
build / build (push) Has been cancelled
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 解析
Some checks are pending
build / build (push) Waiting to run
2025-02-12 19:21:24 +08:00
xream
71fc9affbf feat: 支持 v2ray SOCKS URI 的输入和输出
Some checks are pending
build / build (push) Waiting to run
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 覆写配置无法使用普通脚本的问题
Some checks are pending
build / build (push) Waiting to run
2025-02-11 13:18:42 +08:00
xream
7002eee88d feat: 调整 Egern VMess 传输层
Some checks are pending
build / build (push) Waiting to run
2025-02-10 21:02:40 +08:00
xream
bd21d58fe7 feat: VMess/VLESS 校验 uuid
Some checks are pending
build / build (push) Waiting to run
2025-02-10 13:34:58 +08:00
xream
2ea46dcbf1 feat: Shadowsocks URI 部分逻辑修正
Some checks are pending
build / build (push) Waiting to run
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: 同步配置逻辑优化
Some checks are pending
build / build (push) Waiting to run
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 逻辑
Some checks failed
build / build (push) Has been cancelled
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
Some checks are pending
build / build (push) Waiting to run
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
Some checks failed
build / build (push) Has been cancelled
2025-02-03 13:47:17 +08:00
xream
2a76eb6462 feat: mihomo snell 版本小于 3 的节点, 强制去除 udp 字段, 防止内核报错
Some checks are pending
build / build (push) Waiting to run
2025-02-02 18:59:14 +08:00
xream
9ac5e136a6 feat: 去除订阅流量信息中空字段, 增强兼容性 2025-02-02 18:39:46 +08:00
xream
38f5a97a20 fix: 修复 Surge 输入的 tfo
Some checks failed
build / build (push) Has been cancelled
2025-01-31 15:14:19 +08:00
xream
14a3488ce2 fix: 修复 Egern 和 Stash 可根据 User-Agent 自动包含官方/商店版/未续费订阅不支持的协议
Some checks failed
build / build (push) Has been cancelled
2025-01-26 20:41:57 +08:00
xream
6afec4f668 feat: Egern 增加 TUIC
Some checks failed
build / build (push) Has been cancelled
2025-01-23 08:22:48 +08:00
xream
b1874e510d feat: 支持 VLESS XHTTP extra
Some checks are pending
build / build (push) Waiting to run
2025-01-22 09:43:43 +08:00
xream
48aaaf5c99 doc: README 2025-01-21 12:02:49 +08:00
xream
7385e17a4c fix: 修复 Base64 合法性判断
Some checks failed
build / build (push) Has been cancelled
2025-01-17 16:34:30 +08:00
xream
c3daea55ab feat: Loon 节点支持 ip-mode
Some checks failed
build / build (push) Has been cancelled
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 的问题
Some checks failed
build / build (push) Has been cancelled
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
Some checks are pending
build / build (push) Waiting to run
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
Some checks are pending
build / build (push) Waiting to run
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
Some checks failed
build / build (push) Has been cancelled
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: 日志中增加上传配置的响应
Some checks failed
build / build (push) Has been cancelled
2025-01-09 18:56:36 +08:00
xream
01eb69d8ae ci: GitHub Action
Some checks are pending
build / build (push) Waiting to run
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
Some checks are pending
build / build (push) Waiting to run
2025-01-08 22:52:00 +08:00
xream
589a6bfadb feat: Base64 Pre-processor 检测解码是否正常
Some checks are pending
build / build (push) Waiting to run
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 名称适配大小写和别名
Some checks failed
build / build (push) Has been cancelled
2025-01-03 23:37:50 +08:00
xream
95b7557635 feat: Loon 正式支持 Shadowsocks 2022
Some checks failed
build / build (push) Has been cancelled
2024-12-31 23:24:35 +08:00
xream
14ca62db4a doc: demo.js
Some checks are pending
build / build (push) Waiting to run
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 参数为空的情况
Some checks are pending
build / build (push) Waiting to run
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
xream
3aedd5943d feat: sing-box includeUnsupportedProxy 开启支持 Hysteria2 端口跳跃 2024-12-29 16:07:33 +08:00
xream
222551eb20 feat: Egern 增加默认 sni 2024-12-28 21:00:40 +08:00
xream
0d5e1ab38b feat: 下载订阅的日志中增加请求 target 和实际输出 2024-12-28 17:42:26 +08:00
xream
a3ec98caa9 feat: Clash 订阅仅缓存 proxies 数据 2024-12-27 21:55:13 +08:00
xream
d9e4d814bb feat: geo 更新 2024-12-27 21:35:51 +08:00
xream
e843aa3702 feat: geo 更新 2024-12-26 03:40:53 +08:00
xream
66464645f2 feat: UDP 协议跳过设置 utls 2024-12-24 21:43:23 +08:00
xream
9ccd6b3816 doc: demo.js 2024-12-24 20:49:41 +08:00
xream
74be1e3d82 doc: README 2024-12-24 15:10:38 +08:00
xream
6d78eb7356 feat: Clash 系输入支持 mieru; 调整 juicity 和 mieru 相关过滤逻辑 2024-12-24 15:08:28 +08:00
xream
38eccca8b4 feat: 组合订阅支持手动设置流量信息. 支持使用链接. 此时使用响应内容 2024-12-24 01:20:38 +08:00
xream
33e5aeceb5 fix: 修复订阅不存在时不打印错误日志的问题 2024-12-23 14:14:10 +08:00
xream
837667edc9 feat: 手动设置流量信息时, 支持使用链接. 此时使用响应内容 2024-12-22 21:57:01 +08:00
xream
0069b0ce83 feat: sing-box 支持 detour 参数(之前只能用 underlying-proxy 或 dialer-proxy 来设置) 2024-12-22 20:06:00 +08:00
xream
fcc9d047ae fix: 修复 edns sourcePrefixLength 2024-12-21 21:13:09 +08:00
xream
382d22e622 feat: 支持 socks5, socks5+tls, http, https(便于输入) 格式输入 2024-12-16 21:06:46 +08:00
xream
06f3e97af2 feat: 支持 Shadowsocks 2022 的 URI 输入/输出 2024-12-15 23:03:41 +08:00
xream
bd87e9231e fix: 修复 Surge SOCKS5 解析 2024-12-13 02:27:03 +08:00
xream
d1d6d19542 feat: Mihomo 支持 direct 2024-12-12 18:35:55 +08:00
xream
08bf0b78bb feat: Surge 支持 direct 2024-12-12 18:22:25 +08:00
xream
9a3cd4f57c feat: 处理状态码 2024-12-12 15:35:19 +08:00
xream
d015c7867e fix: 修复 SS URI 解析 2024-12-08 11:24:46 +08:00
xream
4713b63083 feat: Loon 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2024-12-05 23:50:51 +08:00
xream
dbf9e7c360 feat: 优化去除无效节点逻辑 感谢群友 Cooip JM 2024-12-05 12:45:07 +08:00
xream
4ea84118c4 feat: gRPC 支持 authority 2024-12-05 00:52:11 +08:00
xream
dda8113a42 feat: 增加 subscription-userinfo 兼容性 2024-12-04 00:56:53 +08:00
xream
f16b2d34f1 feat: geo 更新 2024-11-30 13:58:04 +08:00
xream
5b28e1a4c9 feat: 支持禁用节点操作 2024-11-29 21:03:29 +08:00
xream
8d0a71d983 feat: VMess URI 输出支持 alterId; Trojan 支持 fp 和 alpn 2024-11-28 16:04:52 +08:00
xream
815552d470 feat: 找不到资源时不通知, 仅保留日志 2024-11-28 15:44:55 +08:00
xream
9d90369594 feat: Trojan URI 支持省略端口号 2024-11-28 13:15:22 +08:00
xream
6aece471aa feat: Stash 使用 includeUnsupportedProxy 参数开启 Shadowsocks 2022 2024-11-27 15:20:20 +08:00
xream
99396773f6 ci: 去除 GitLab Sync 2024-11-26 15:01:04 +08:00
xream
e229408a2d feat: 默认缓存阈值 1024KB 2024-11-24 12:31:18 +08:00
xream
514414587b feat: 默认超时 8000ms 2024-11-24 12:13:52 +08:00
egerndaddy
d4c419745e Update Egern.yaml 2024-11-22 23:59:59 +08:00
xream
fe3da254f4 feat: 支持 Egern 输出 2024-11-21 13:16:04 +08:00
xream
7d8132d7cd feat: 默认输出格式改为 V2Ray; accept 为 application/json 时, 输出 JSON; 响应增加 X-Powered-By Sub-Store 2024-11-19 23:06:45 +08:00
xream
bc1247efaf feat: 手动设置的订阅流量信息会附加到订阅自己的流量信息之前 2024-11-17 23:40:03 +08:00
xream
dea937df66 feat: 默认查询流量信息的 User-AgentQuantumult%20X/1.0.30 (iPhone14,2; iOS 15.6) 改为 clash; 流量信息缓存逻辑调整 2024-11-17 02:10:38 +08:00
xream
cfb5a8e082 feat: 支持解析订阅中的 profile-web-page-url 字段 2024-11-17 01:02:28 +08:00
xream
4790bf47d1 feat: Surge 密码解析支持首尾成对的单引号双引号, 输出时增加双引号 2024-11-16 21:50:52 +08:00
xream
56fd495fb6 feat: 支持更多的 subscription-userinfo 2024-11-12 22:20:46 +08:00
xream
f4639d9a34 feat: 支持更多的 subscription-userinfo 2024-11-12 22:06:22 +08:00
xream
cc58a5541e feat: 订阅刷新按钮逻辑调整为无缓存刷新订阅和流量 2024-11-10 01:22:48 +08:00
xream
772f431887 feat: 模块版文件中增加 token 路由 2024-11-08 18:10:39 +08:00
xream
2b60c515cd feat: 支持管理 token 2024-11-04 13:59:57 +08:00
xream
c8c22c3901 fix: 修复 VMess URI SNI 2024-11-01 20:27:23 +08:00
xream
d8f9466b84 feat(wip): 支持自定义 share token 2024-10-31 23:33:34 +08:00
xream
d12ccad382 feat: MMDB 加入 $utils.ipasn 2024-10-31 01:39:13 +08:00
xream
b4358663cc feat(wip): 支持 JWT 2024-10-31 00:23:45 +08:00
xream
aba6264988 feat(wip): 支持 JWT 2024-10-30 23:08:01 +08:00
xream
2320ab3838 feat(wip): 支持 JWT 2024-10-30 22:51:31 +08:00
xream
542957d34a feat(wip): 支持 JWT 2024-10-30 22:27:39 +08:00
xream
07e50175f9 feat: cipher 应为小写 2024-10-30 16:07:27 +08:00
xream
e09d66060d feat: 远程订阅支持 insecure 不验证服务器证书 2024-10-30 14:33:34 +08:00
xream
b048ecdfff fix: 修复 surge mac 未开启 mihomo 时, 对于不支持的节点未报错, 导致出现 proxy 为 undefined 的问题 2024-10-29 18:31:02 +08:00
xream
aac72fb9a3 feat: Surge 支持 udp-port, 修复 udp-relay 参数解析 2024-10-27 19:00:42 +08:00
xream
baec193e5c feat: 支持 VLESS mKcp 2024-10-23 17:38:59 +08:00
xream
8fe818f826 fix: 处理乱填的订阅流量信息解析报错 2024-10-19 19:12:25 +08:00
xream
72286984ec fix: 修复 YAML 处理 undefined 的问题 2024-10-18 12:38:58 +08:00
xream
27e693c308 feat: ⚠️ BREAKING CHANG 仅手动指定 target 为 SurgeMac 时, 启用 mihomo 来支援 Surge 本身不支持的协议 2024-10-17 20:26:07 +08:00
xream
6cf8080cd3 fix: 修复 VMess VLESS servername 2024-10-17 14:01:44 +08:00
xream
839fcacf63 fix: 修复传输层和 SNI 的问题(有问题麻烦即时反馈 谢谢) 2024-10-16 21:31:41 +08:00
xream
a2e45bcb10 commit
feat: Surge 支持 Shadowsocks 2022(为了兼容 必须使用 `includeUnsupportedProxy` 参数或开启 `包含官方/商店版不支持的协议` 开关)
2024-10-15 17:00:13 +08:00
xream
ea0eb91691 doc: README 2024-10-12 14:51:58 +08:00
xream
1f0ddf2d28 fix: 修复组合订阅预览 2024-10-12 10:46:39 +08:00
xream
a660c6ff90 feat: 组合订阅支持通过单条订阅的标签进行关联 2024-10-11 20:57:45 +08:00
xream
71d9adbc07 chore: bump release version 2024-10-11 20:00:09 +08:00
pillarcoin
97bec9183a fix: clash 配置中 VLESS 节点的 short-id 值被错误解析 2024-10-11 19:10:02 +08:00
xream
ef85b6d0e9 feat: 文件支持设置代理/策略, 链接支持传入 proxy 参数指定代理/策略; 修复代理/策略优先级 2024-10-07 22:05:07 +08:00
xream
8ffb060cb4 feat: 组合订阅支持设置代理/策略, 链接支持传入 proxy 参数指定代理/策略 2024-10-07 20:56:33 +08:00
xream
6d43961e96 feat: Node.js 支持使用环境变量 SUB_STORE_BACKEND_DEFAULT_PROXY 设置默认代理; ProxyUtils 增加 download 方法 2024-10-07 18:43:29 +08:00
xream
f3200aea8c feat: 流量和同步配置也使用默认代理/策略 2024-10-07 18:34:39 +08:00
xream
e2346d16a2 feat: 新增全局代理/策略设置, 前端 > 2.14.265 2024-10-07 18:05:06 +08:00
xream
dc320eaa6c feat(file): 新增启用下载(文件名为显示名称), 前端 > 2.14.264 2024-10-07 17:26:15 +08:00
xream
02031019f7 doc: demo.js 2024-09-22 06:02:58 +08:00
xream
5d09fe782f feat: 增加 _subDisplayName _collectionDisplayName 2024-09-18 19:42:53 +08:00
xream
6e425e5908 doc: demo.js 2024-09-18 19:22:12 +08:00
xream
d10c9233c0 feat: 正式弃用旧的 subName 和 collectionName 2024-09-18 19:18:50 +08:00
xream
cc556b641d fix: 修复 password 为数字时的 bug 2024-09-16 01:43:16 +08:00
xream
de2813b035 feat: 使用自定义缓存时 cacheKey 的值不能为空 2024-09-13 23:42:55 +08:00
xream
c9158ceb1d feat: 内置的 Google/Cloudflare DNS 更换为 DoH 2024-09-09 14:56:47 +08:00
xream
5cf0c98f5f chroe: 修改脚本链接为 release 分支 2024-09-07 23:21:42 +08:00
xream
7d0414f8ca fix: 传输层 path 应为以 / 开头的字符串 2024-09-05 17:39:42 +08:00
xream
bee1d62a1a fix: 传输层 path 应以 / 开头 2024-09-05 17:15:30 +08:00
xream
72bc9b9456 feat: 处理非字符串的 ports 字段 2024-09-04 13:40:17 +08:00
xream
3b4c14e7d0 doc: README 2024-09-04 10:49:52 +08:00
xream
59d93483bb feat: Node.js 版支持环境变量 SUB_STORE_BACKEND_DOWNLOAD_CRON 设置定时恢复配置, SUB_STORE_BACKEND_UPLOAD_CRON 设置定时备份配置, SUB_STORE_BACKEND_SYNC_CRON 设置定时同步订阅/文件 2024-09-04 02:20:28 +08:00
xream
75d88c02c7 feat: SurgeMac 支持使用 mihomo 来支援 Surge 本身不支持的协议; 弃用旧的 ssr-local 方案 2024-09-03 20:31:42 +08:00
xream
99d5868cff feat: 订阅和文件的请求链接支持传入 $options , 可在脚本中使用 2024-09-03 13:58:10 +08:00
xream
e1489a3cf7 feat: sing-box VLESS Reality uTLS 默认启用 2024-09-02 21:20:22 +08:00
xream
59fe16a7b0 feat: Surge Hysteria2 与 TUIC 协议支持端口跳跃; Hysteria2 URI 的端口部分支持 端口跳跃 的「多端口地址格式」 2024-09-02 16:38:21 +08:00
xream
562d349629 feat: 脚本操作传入上下文 require (仅对应的环境支持)" 2024-08-31 22:39:54 +08:00
egerndaddy
9ce14351c5 doc: 添加 Egern 模块链接 2024-08-29 13:26:27 +08:00
egerndaddy
76e781c711 Create Egern.yaml 2024-08-29 13:09:59 +08:00
xream
f0acf4a2a7 fix: DoH 结果过滤 2024-08-29 12:30:49 +08:00
xream
9abeb4ce7b fix: 修复 SurgeMac ShadowsocksR obfs-param 2024-08-28 14:51:06 +08:00
xream
153802c7c4 feat: Loon SOCKS5 UDP 2024-08-26 00:33:22 +08:00
xream
19418b631f feat(uri): VMess URI 输入支持 allowInsecure(输出不支持, 与 2dust/v2rayN 分享链接逻辑一致) 2024-08-18 15:53:13 +08:00
xream
97caeed208 feat(geo): 增加 利雅得 Riyadh 2024-08-17 14:06:28 +08:00
xream
dd8d1d85e8 feat: 支持 Loon tls-pubkey-sha256, tls-cert-sha256 2024-07-30 22:17:25 +08:00
xream
14ed56b5d5 chore: 传输层应该有配置, 暂时不考虑兼容不给配置的节点 2024-07-24 11:27:33 +08:00
xream
9785271c5b chore: 增加部分 clash.meta(mihomo) 内核客户端的 User-Agent(clash-verge, flclash) 2024-07-20 14:48:39 +08:00
xream
05bdf95a29 feat: 处理端口跳跃(感谢亚托莉佬) 2024-07-19 15:23:44 +08:00
xream
317a804b36 fix: 修复 URI 报错 2024-07-19 14:33:34 +08:00
xream
10ec8a25a2 feat: 处理不规范的 hysteria2 节点 2024-07-19 09:45:28 +08:00
xream
aa0943a909 fix: 被识别为 IP4P 的域名解析结果均增加 _IP4P 字段; 修复报错 2024-07-18 19:48:01 +08:00
xream
a0c1bbbf70 fix: 域名解析修复; 结果增加 _IP4P 字段 2024-07-18 19:42:57 +08:00
xream
fea9de4fae feat: IP4P 合并进 IPv6; ProxyUtils 中增加 ipAddress 2024-07-18 18:35:22 +08:00
xream
cddd1818fe chore: bump release version 2024-07-08 02:51:21 +08:00
xream
f94830b2df Merge pull request #339 from zhiqiang02/add-tai-wan-keyword
Add 'Tai Wan' as a keyword for Taiwan flag
2024-07-08 02:50:13 +08:00
zhiqiang02
e02a26040e Add 'Tai Wan' as a keyword for Taiwan flag
碰到了某机场奇奇怪怪的节点名
2024-07-08 02:38:50 +08:00
xream
6906efdd55 chore: bump release version 2024-07-02 21:04:14 +08:00
xream
9558b63261 Merge pull request #336 from cooip-jm/patch-1
处理grpc-opts为 {} 的情况
2024-07-02 21:03:30 +08:00
cooip-jm
4bfdef17ee 处理grpc-opts为 {} 的情况
该字段仅影响sing-box内核,对mihomo无影响
2024-07-02 21:01:11 +08:00
xream
9d29fc8a09 feat: 处理 reality-opts 为 {} 的情况 2024-07-02 20:39:04 +08:00
xream
f524920c13 feat: 文件支持设置 查询流量信息订阅链接. 服务器版中使用此链接可在响应中传递订阅流量信息 2024-06-28 18:34:26 +08:00
xream
bfe072cbdf feat: 域名解析支持自定义 EDNS(需新版前端) 2024-06-22 11:45:37 +08:00
xream
32dcca4a26 feat: 域名解析支持自定义 DoH(需新版前端) 2024-06-20 21:42:15 +08:00
xream
a5d77c39c8 feat: 域名解析增加超时参数(默认使用全局超时) 2024-06-20 13:41:05 +08:00
xream
6ea1b69a62 doc: demo.js 增加更多字段的说明 2024-06-20 11:44:07 +08:00
xream
2b3b9177e5 feat: 域名解析新增 _resolved_ips 为解析出的所有 IP 2024-06-20 11:28:17 +08:00
xream
91aab3ca7a fix: 修复 Tencent DNS 缓存 2024-06-20 10:59:06 +08:00
xream
c1a9fc6abc fix: 修复 Loon Hysteria2 salamander 混淆 2024-06-17 11:08:43 +08:00
xream
11d9ce7372 feat: 支持 Loon Hysteria2 salamander 混淆 2024-06-16 21:49:13 +08:00
xream
ad3d2270ac feat: 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint 2024-06-13 20:44:12 +08:00
xream
3ad42f2c10 feat: Stash 支持 juicity, ssh 2024-06-12 15:16:56 +08:00
xream
ec06eb8659 fix: sing-box tls cert 应该为数组 2024-06-10 19:10:57 +08:00
xream
4a23a4d8b6 fix: tlsParser typo 2024-06-10 19:07:19 +08:00
xream
913638a233 feat: /api/sub/flow/:name 接口支持指定远程订阅 url(可携带订阅 url 支持的参数, 例如 flowUserAgent) 2024-06-10 13:24:06 +08:00
xream
bf642ce0e6 fix: 兼容空的订阅链接 2024-06-09 01:42:40 +08:00
xream
1ecac9da92 chore: demo.js 2024-06-06 21:50:13 +08:00
xream
c5a417da8f feat: VMess URI 支持 TCP/H2 传输层 2024-06-03 21:14:07 +08:00
xream
8cd0545023 feat: ws, http, h2 传输层补全 path 2024-06-03 00:34:03 +08:00
xream
b6f848a6e6 feat: ProxyUtils.removeFlag 2024-06-02 18:30:53 +08:00
xream
99d058bcf1 feat: 支持 flowUrl 2024-06-02 16:03:01 +08:00
xream
533103e765 feat: 进一步优化乐观缓存和同步配置的逻辑 2024-06-01 20:09:57 +08:00
xream
cf82764171 feat: 进一步优化乐观缓存和同步配置的逻辑 2024-06-01 19:50:16 +08:00
xream
7b783c1fe3 fix: 简单修复乐观缓存(当异步更新乐观缓存时, 若存在常规缓存, 将使用常规缓存) 2024-05-31 20:52:01 +08:00
xream
372eff9a44 chore: 文案 2024-05-31 11:42:23 +08:00
xream
d3b5a529d7 doc: README 2024-05-30 21:32:48 +08:00
xream
8049134bb5 feat: Surge includeUnsupportedProxy 去除 HTTP 传输层(不一定能通, 由服务端配置确定) 2024-05-30 18:41:56 +08:00
xream
3f620700a4 feat: GUIforCores 请求增加参数 proxy, timeout 2024-05-30 17:19:38 +08:00
xream
9e64a68481 fix: VMess URI 输入传输层为 HTTP 时, path 默认为 / 2024-05-30 14:28:02 +08:00
xream
9ce5916414 fix: 乐观缓存未捕获错误 2024-05-30 13:10:08 +08:00
xream
047c21fe70 feat: 节点上的额外参数调整为下划线开头, 原参数目前仍保留, 若有脚本需要使用这些参数请尽快修改(_subName, _collectionName, _resolved, _no-resolve) 2024-05-30 04:48:13 +08:00
xream
47849dc6d0 feat: 节点上的额外参数调整为下划线开头, 原参数目前仍保留, 若有脚本需要使用这些参数请尽快修改(_subName, _collectionName, _resolved) 2024-05-30 04:28:54 +08:00
xream
af06086c1b chore: 去除 Surge/Surfboard 输出节点名中的逗号和等号 2024-05-29 19:15:52 +08:00
xream
4a6a147667 feat: 新增 定时处理订阅 功能, 避免 App 内拉取超时 2024-05-28 12:05:35 +08:00
xream
c6540d14cd feat: Surge Beta 模块支持定时处理订阅. 一般用于定时处理耗时较长的订阅, 以更新缓存. 这样 Surge 中拉取时就能用到缓存, 不至于总是超时 2024-05-28 02:31:25 +08:00
xream
3db71ec531 fix: Base64 输入支持 hy2:// 2024-05-26 10:28:46 +08:00
xream
cf156c2f17 fix: Stash 服务器证书 SHA256 指纹字段为 server-cert-fingerprint 2024-05-25 18:54:51 +08:00
xream
e28e2a78fb feat: 下载订阅日志中增加请求的 User-Agent 2024-05-25 18:29:48 +08:00
xream
b0a2c709e8 fix: Stash 服务器证书 SHA256 指纹字段为 server-cert-fingerprint 2024-05-21 11:05:45 +08:00
xream
5dc2c8ced7 chore: demo.js 2024-05-20 17:42:09 +08:00
xream
d2a65ee0fe chore: demo.js 2024-05-20 17:38:49 +08:00
xream
4dd4ae98ca chore: 最新版 Surge 已删除 ability: http-client-policy 参数, 模块暂不做修改, 对测落地功能无影响 2024-05-18 23:57:18 +08:00
xream
0d41eb467f feat: 某些域名仅支持从国内 DNS 解析正确结果, 为方便部署在海外的用户, 使用国内 DNS 解析时, ECS IP 指定为国内 IP 2024-05-18 22:24:58 +08:00
xream
ba1c91a7a5 chore: 文案 2024-05-17 17:49:10 +08:00
xream
30fa87c172 doc: Shadowrocket 模块 2024-05-15 20:58:55 +08:00
xream
1eaa33948b chore: bump release version 2024-05-14 20:45:39 +08:00
xream
619e256ed8 Merge pull request #322 from onejibang/master
chore: Change network request method
2024-05-14 20:45:00 +08:00
onejibang
b46209e164 chore: Change network request method 2024-05-14 16:51:05 +08:00
xream
a1ba4e273e chore: bump release version 2024-05-13 19:44:27 +08:00
xream
bfc95ed92a Merge pull request #320 from onejibang/feature-gui-for-cores
feat: 适配GUI.for.Cores项目组下的客户端程序
2024-05-13 19:41:45 +08:00
xream
32f591ec56 Merge branch 'master' into feature-gui-for-cores 2024-05-13 19:41:28 +08:00
onejibang
cea16d8c44 chore: Canonical variable name 2024-05-13 19:22:33 +08:00
onejibang
93a1ba7b50 feat: support HEAD method 2024-05-13 19:12:22 +08:00
xream
e6d1aa1150 feat: 使用了自定义缓存 cacheKey 的远程订阅 调整为乐观缓存 2024-05-13 17:08:17 +08:00
onejibang
26e83798da feat: Provide virtual disk operation API 2024-05-13 16:35:05 +08:00
xream
b083d2d840 feat: Node.js 版支持 MMDB, 通过环境变量或在脚本中传入数据库文件路径, 可使用 ipaso 和 geoip 方法 2024-05-12 23:17:11 +08:00
onejibang
cf35afcab2 Adapted for GUI.for.Cores 2024-05-11 17:32:01 +08:00
xream
d073dfeef8 feat: 支持 Trojan, VMess, VLESS httpupgrade(暂不支持 Shadowsocks v2ray-plugin) 2024-05-10 10:15:11 +08:00
Peng-YM
f970ea3361 Update README.md 2024-05-09 09:38:36 +08:00
xream
630bac0575 fix: 简单修复 SS URI 多参数拼接 2024-05-05 02:14:55 +08:00
xream
7f3cb2b191 feat: 当无插件参数时, 去除 SS URI 输出中的 / 以兼容部分客户端 2024-05-05 02:11:50 +08:00
xream
92e1e4a0fb feat: ProxyUtils 中增加 Gist 类; 补充 demo.js 中的示例 2024-05-04 21:35:27 +08:00
xream
3b85d313fe fix: 兼容不规范的 QX URI 2024-05-03 03:56:59 +08:00
xream
c91d8e28e4 fix: 哪吒探针在线时长 2024-04-30 15:39:14 +08:00
xream
8cbb4492be feat: 全部是 WireGuard 节点的订阅, 支持输出为 Surge 模块 2024-04-25 16:55:32 +08:00
xream
6f7da57e3a fix: 旗帜操作中将 🏴‍☠️ 🏳️‍🌈 也视为已有旗帜并在删除后添加新旗帜 2024-04-25 13:38:01 +08:00
xream
2586c29746 fix: 处理手动删除 Gist 之后, Sub-Store 侧重新同步的逻辑 2024-04-23 09:28:39 +08:00
xream
6f7fe8204b feat: 支持完整导出和导入 Sub-Store 单条订阅数据 2024-04-22 15:15:57 +08:00
xream
bafaf07743 Merge pull request #314 from eric-gitta-moore/master
feat: script ip-flag for node
2024-04-22 13:09:33 +08:00
Eric Moore
9962eb0947 feat: script ip-flag for node 2024-04-22 12:59:06 +08:00
xream
ac5232a7bc ci: restore the functionality of generating conventional changelog 2024-04-22 04:25:19 +08:00
xream
2301ccbfb5 fix: 修复对不规范的节点名称的处理 2024-04-22 02:51:44 +08:00
xream
0b5761e5fc feat: QX 输出正式支持 VLESS 2024-04-22 02:15:54 +08:00
xream
3ab21b0e26 feat: 支持 Loon SOCKS5/SOCKS5-TLS 2024-04-21 12:36:11 +08:00
xream
89ab72e46c doc: README 2024-04-21 11:32:23 +08:00
xream
18bd6526d0 chore: 增加探针版本(没有自定义的必要吧 默认为 0.0.1) 2024-04-20 07:45:49 +08:00
xream
c7329c32eb feat: 哪吒探针网络监控接口支持用参数传入检测次数; 节点字段上自定义的多个次数, 只取最大值 2024-04-19 06:27:45 +08:00
xream
4819ae95e4 feat: 哪吒探针网络监控接口提示不兼容的节点, 支持传入节点名 2024-04-19 05:57:26 +08:00
xream
370d228b04 feat: 订阅兼容哪吒探针网络监控接口(Loon/Surge 可输出节点延迟) 2024-04-19 05:16:24 +08:00
xream
d092916168 fix: sing-box wireguard 2024-04-17 11:36:23 +08:00
xream
0c93de48ab feat: GEO 增加 TYO 2024-04-17 05:50:50 +08:00
xream
274aa50373 ci: GitHub Actions: Transitioning from Node 16 to Node 20 2024-04-17 01:12:07 +08:00
xream
e24de8d0b6 feat: 支持 WireGuard URI 输入和输出 2024-04-17 00:56:53 +08:00
xream
93a5ce6c3b feat: 支持 dialer-proxy, detour 2024-04-14 21:34:45 +08:00
xream
cb66c8daa2 feat: fancy-characters 增加 modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代) 2024-04-12 22:39:59 +08:00
xream
f4cdc953e6 feat: 支持设置并在远程订阅失败时读取最近一次成功的缓存 2024-04-09 20:49:42 +08:00
xream
2a1c2eb9df chore: 处理订阅输出哪吒探针兼容响应的 Uptime 字段 2024-04-09 13:48:33 +08:00
xream
6217c2e5cd fix: 修复 sing-box wireguard reserved 2024-04-07 19:48:08 +08:00
xream
f90d9c2fd1 chore: Surge 模块文案 2024-04-07 16:40:09 +08:00
xream
3e952e9e88 chore: Surge 默认模块更新为支持编辑参数的版本 2024-04-06 19:40:30 +08:00
xream
a81b55f752 doc: demo.js 2024-04-05 13:46:07 +08:00
xream
33652af516 feat: 订阅支持输出哪吒探针兼容响应; 清理输出数据; 增加内部数据字段 2024-04-05 13:37:15 +08:00
xream
2bca669930 fix: 修复 SS URI 解析错误 2024-04-04 16:52:46 +08:00
xream
f1bf0e1e8d fix: 修复 Tencent DNS 解析 2024-04-04 15:26:19 +08:00
xream
16b9cd9aaf feat: GEO 增加 AMS 2024-04-03 00:45:48 +08:00
xream
32eb069ab2 feat: GEO 增加 JNB, SJC, SEL 2024-04-02 21:06:56 +08:00
xream
4c9f8011c7 fix: 修复内蒙古识别为蒙古的问题 2024-04-02 20:56:07 +08:00
xream
bd26b0a561 feat: 区域过滤增加韩国德国 2024-04-02 20:40:13 +08:00
xream
958d1e52c8 chore: 日志输出增加订阅的来源 User-Agent 2024-03-31 10:59:49 +08:00
xream
e7a2e60963 feat: sing-box 订阅格式修改(如需原始格式 请使用 target=sing-box&produceType=internal); 清理 Clash 系无效字段 2024-03-31 09:36:32 +08:00
xream
fa6a274f79 feat: ProxyUtils 增加 getFlag, getISO 方法 2024-03-31 08:20:51 +08:00
xream
e40b3f88d5 chore: geo 增加关键词 德意志 2024-03-31 06:59:31 +08:00
xream
163ad9ee09 feat: JSON 输出支持 produceType internal 2024-03-31 04:45:57 +08:00
xream
abb6f2dec1 feat: 处理 sni off 的情况. 若出现问题, 麻烦大家及时反馈 2024-03-30 01:12:34 +08:00
xream
56870bbd5f fix: 修复组合订阅子订阅失败导致预览失败 2024-03-26 14:59:24 +08:00
xream
efbc6ecd84 feat: 流量单位显示由 EB 提升到 YB 2024-03-25 03:50:47 +08:00
xream
c27c589024 chore: 调整部分日志 2024-03-25 02:47:06 +08:00
xream
0efed4f1a0 feat: 处理传入 httpClient 的 timeout 参数 2024-03-24 07:28:16 +08:00
xream
e3a514d1fb feat: hysteria2 支持 mport, clash.meta(mihomo) 支持 ports 2024-03-21 20:07:24 +08:00
xream
64478c7a27 feat: 刷新时 清除所有缓存 2024-03-21 02:37:21 +08:00
xream
dc8f19f350 doc: demo.js 2024-03-21 01:52:42 +08:00
xream
b4ccfc7e07 chore: Sub-Store Simple 脚本增加脚本超时(默认 120) 可能会影响某些逻辑 待观察 2024-03-19 23:48:26 +08:00
xream
3f1940630a chore: 增加 gist 错误日志 2024-03-19 21:30:52 +08:00
xream
5a0bdb1276 doc: demo.js 2024-03-18 20:24:18 +08:00
xream
a1b86e26a2 chore: 增加上传同步配置的详细日志 2024-03-16 02:47:01 +08:00
xream
6ec8c29f6a feat: 规则中处理 GEOIP/GEOSITE, Loon 已支持 SRC-PORT/DEST-PORT/PROTOCOL 2024-03-15 08:49:36 +08:00
xream
bbb9602f9f release: backend version 2.14.256 2024-03-15 08:12:06 +08:00
xream
6db6153672 Merge pull request #295 from makaspacex/master
[Feat]规则转换增加对GEOIP与GEOSITE的支持
2024-03-15 08:10:30 +08:00
makabaka
b66189948a 规则转换增加对GEOIP与GEOSITE的支持 2024-03-14 22:07:45 +08:00
xream
2611dccc73 feat: 支持设置查询远程订阅流量信息时的 User-Agent 2024-03-14 19:45:39 +08:00
xream
25d3cf6ca4 feat: 通过代理/节点/策略获取订阅 现已支持 Surge, Loon, Stash, Shadowrocket, QX, Node.js 2024-03-14 01:54:07 +08:00
xream
3637c5eb74 feat: SSH 协议跟进 clash.meta(mihomo) 的修改 2024-03-13 16:24:30 +08:00
xream
80d46597b4 feat: 支持使用代理/节点/策略获取订阅 2024-03-13 05:33:52 +08:00
xream
ca65e4209e feat: 支持自定义订阅流量信息 2024-03-12 01:17:56 +08:00
xream
53bb4866e7 fix: 修复订阅流量传递 2024-03-12 00:55:30 +08:00
xream
09495fa607 fix: 修复重置天数微妙的偏差 2024-03-11 19:33:51 +08:00
xream
4b27d40602 feat: 订阅支持开始日期和重置周期 2024-03-11 13:39:52 +08:00
xream
518de2e919 feat: 订阅支持每月重置天数 2024-03-10 23:08:56 +08:00
xream
078bf228de feat: produceArtifact 方法支持传入自定义 subscription; VLESS 非 reality 删除空 flow 2024-03-10 17:22:25 +08:00
xream
aaef97cf5d feat: SSH 新增 clash.meta(mihomo), 调整 Surge 和 sing-box 2024-03-08 19:01:01 +08:00
xream
7beff4013f feat: 订阅列表的流量信息兼容远程和本地合并的情况, 排除设置了不查询订阅信息的链接 2024-03-08 18:40:44 +08:00
xream
23cf81d0a5 feat: Node.js 版 /api/utils/env 增加 meta 信息 2024-03-08 14:20:55 +08:00
xream
572f2f5533 feat: OpenAPI 增加 isEgern, isLanceX; /api/utils/env 增加 meta 信息 2024-03-08 13:56:59 +08:00
xream
1c6d761e09 fix: 修复 Surge WireGuard allowed-ips 双引号 2024-03-07 17:24:49 +08:00
xream
437297b8b0 feat: 增加下载缓存阈值 2024-03-05 05:03:17 +08:00
xream
ca437865e6 feat: 域名解析新增 IP4P, 支持禁用缓存 2024-03-05 01:01:46 +08:00
xream
739100c873 feat: Stash/clash.meta(mihomo) 支持 interface-name 字段 2024-03-04 11:43:07 +08:00
xream
a4384f4f13 fix: 修复 Clash 节点名为 binary 的情况 2024-03-03 14:33:49 +08:00
xream
468d136f0e ci: git push assets to "release" branch 2024-02-28 23:07:16 +08:00
86 changed files with 15583 additions and 10249 deletions

View File

@@ -1,5 +1,6 @@
name: build
on:
workflow_dispatch:
push:
branches:
- master
@@ -21,37 +22,43 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "16"
node-version: "20"
- name: Install dependencies
run: |
npm install -g pnpm
cd backend && pnpm i
- name: Test
run: |
cd backend
pnpm test
- name: Build
run: |
cd backend
pnpm run build
cd backend && pnpm i --no-frozen-lockfile
# - name: Test
# run: |
# cd backend
# pnpm test
# - name: Build
# run: |
# cd backend
# pnpm run build
- name: Bundle
run: |
cd backend
pnpm run bundle
pnpm bundle:esbuild
- id: tag
name: Generate release tag
run: |
cd backend
SUBSTORE_RELEASE=`node --eval="process.stdout.write(require('./package.json').version)"`
echo "release_tag=$SUBSTORE_RELEASE" >> $GITHUB_OUTPUT
- name: Prepare release
run: |
cd backend
pnpm i -D conventional-changelog-cli
pnpm run changelog
- name: Release
uses: softprops/action-gh-release@v1
if: ${{ success() }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ./backend/CHANGELOG.md
tag_name: ${{ steps.tag.outputs.release_tag }}
generate_release_notes: true
# generate_release_notes: true
files: |
./backend/sub-store.min.js
./backend/dist/sub-store-0.min.js
@@ -59,8 +66,19 @@ jobs:
./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
- name: Git push assets to "release" branch
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline
cd backend/dist || exit 1
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b release
git add .
git commit -m "release: ${{ steps.tag.outputs.release_tag }}"
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f -u origin release
# - name: Sync to GitLab
# env:
# GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
# run: |
# curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

0
.gitmodules vendored
View File

View File

@@ -7,13 +7,13 @@
</div>
<p align="center" color="#6a737d">
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
Advanced Subscription Manager for QX, Loon, Surge, Stash, Egern and Shadowrocket.
</p>
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
<a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
Core functionalities:
1. Conversion among various formats.
@@ -21,21 +21,30 @@ Core functionalities:
3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion
### Supported Input Formats
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5)
> ⚠️ Do not use `Shadowrocket` or `NekoBox` to export URI and then import it as input. The URIs exported in this way may not be standard URIs. However, we have already supported some very common non-standard URIs (such as VMess, VLESS).
- [x] Proxy URI Scheme(`socks5`, `socks5+tls`, `http`, `https`(it's ok))
example: `socks5+tls://user:pass@ip:port#name`
- [x] URI(AnyTLS, SOCKS, SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
> Please note, HTTP(s) does not have a standard URI format, so it is not supported. Please use other formats.
- [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC)
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms
@@ -43,16 +52,21 @@ Core functionalities:
- [x] Plain JSON
- [x] Stash
- [x] Clash.Meta(mihomo)
- [x] Clash
- [x] Surfboard
- [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
- [x] Loon
- [x] Egern
- [x] Shadowrocket
- [x] QX
- [x] sing-box
- [x] V2Ray
- [x] V2Ray URI
Deprecated:
- [x] Clash
## 2. Subscription Formatting
### Filtering
@@ -98,7 +112,7 @@ or
esbuild(experimental)
```
pnpm run --parallel "/^dev:.*/"
SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
```
## LICENSE
@@ -111,8 +125,13 @@ This project is under the GPL V3 LICENSE.
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
- Special thanks to @Orz-3 and @58xinian for their awesome icons.
## Sponsors
[![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 %>
* @version: <%= pkg.version %>
* @author: Peng-YM

View File

@@ -1,7 +1,7 @@
{
"name": "sub-store",
"version": "2.14.230",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"version": "2.19.89",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js",
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -11,24 +11,40 @@
"dev:esbuild": "nodemon -w src -w package.json dev-esbuild.js",
"dev:run": "nodemon -w sub-store.min.js sub-store.min.js",
"build": "gulp",
"bundle": "node bundle.js"
"bundle": "node bundle.js",
"bundle:esbuild": "node bundle-esbuild.js",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
},
"author": "Peng-YM",
"license": "GPL-3.0",
"pnpm": {
"patchedDependencies": {
"http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch"
}
},
"dependencies": {
"@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0",
"buffer": "^6.0.3",
"connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6",
"dns-packet": "^5.6.1",
"dotenv": "^16.4.7",
"express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"fetch-socks": "^1.3.2",
"http-proxy-middleware": "^3.0.3",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2",
"json5": "^2.2.3",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21",
"request": "^2.88.2",
"requests": "^0.3.0",
"semver": "^7.3.7",
"mime-types": "^2.1.35",
"ms": "^2.1.3",
"nanoid": "^3.3.3",
"semver": "^7.6.3",
"static-js-yaml": "^1.0.0",
"uuid": "^8.3.2"
"undici": "^7.4.0"
},
"devDependencies": {
"@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--;
}

16816
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules';
export const TOKENS_KEY = 'tokens';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

View File

@@ -1,6 +1,18 @@
import { Base64 } from 'js-base64';
import { Buffer } from 'buffer';
import rs from '@/utils/rs';
import YAML from '@/utils/yaml';
import download from '@/utils/download';
import { isIPv4, isIPv6, isValidPortNumber, isNotBlank } from '@/utils';
import download, { downloadFile } from '@/utils/download';
import {
isIPv4,
isIPv6,
isValidPortNumber,
isValidUUID,
isNotBlank,
ipAddress,
getRandomPort,
numberToString,
} from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers';
@@ -9,6 +21,11 @@ import $ from '@/core/app';
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
import Gist from '@/utils/gist';
import { isPresent } from './producers/utils';
import { doh } from '@/utils/dns';
import JSON5 from 'json5';
function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) {
@@ -63,23 +80,43 @@ function parse(raw) {
$.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(proxies, operators = [], targetPlatform, source) {
async function processFn(
proxies,
operators = [],
targetPlatform,
source,
$options,
) {
for (const item of operators) {
if (item.disabled) {
$.log(
`Skipping disabled operator: "${
item.type
}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || 'None'
}`,
);
continue;
}
// process script
let script;
let $arguments = {};
if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args;
if (mode === 'link') {
let noCache;
let url = content;
if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, '');
noCache = true;
}
let url = content || '';
// extract link arguments
const rawArgs = url.split('#');
if (rawArgs.length > 1) {
@@ -98,10 +135,17 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
}
}
}
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`;
const downloadUrlMatch = url.match(
/^\/api\/(file|module)\/(.+)/,
);
url = `${url.split('#')[0]}${
rawArgs[2]
? `#${rawArgs[2]}`
: $arguments?.noCache != null ||
$arguments?.insecure != null
? `#${rawArgs[1]}`
: ''
}`;
const downloadUrlMatch = url
.split('#')[0]
.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = '';
try {
@@ -131,6 +175,17 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
script = fs.readFileSync(url.split('#')[0], 'utf8');
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when reading local script: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取脚本文件: ${url}`);
}
} else {
// if this is a remote script, download it
try {
@@ -145,6 +200,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
}
} else {
script = content;
$arguments = item.args.arguments || {};
}
}
@@ -153,7 +209,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
continue;
}
$.info(
$.log(
`Applying "${item.type}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || 'None'
}`,
@@ -165,6 +221,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
targetPlatform,
$arguments,
source,
$options,
);
} else {
processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -180,36 +237,67 @@ function produce(proxies, targetPlatform, type, opts = {}) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
}
// filter unsupported proxies
proxies = proxies.filter(
(proxy) =>
!(proxy.supported && proxy.supported[targetPlatform] === false),
const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
targetPlatform,
);
// filter unsupported proxies
proxies = proxies.filter((proxy) => {
// 检查代理是否支持目标平台
if (proxy.supported && proxy.supported[targetPlatform] === false) {
return false;
}
// 对于 vless 和 vmess 代理,需要额外验证 UUID
if (['vless', 'vmess'].includes(proxy.type)) {
const isProxyUUIDValid = isValidUUID(proxy.uuid);
if (!isProxyUUIDValid)
$.error(`UUID may be invalid: ${proxy.name} ${proxy.uuid}`);
// return isProxyUUIDValid;
}
return true;
});
proxies = proxies.map((proxy) => {
proxy._resolved = proxy.resolved;
if (!isNotBlank(proxy.name)) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
if (proxy['disable-sni']) {
if (sni_off_supported) {
proxy.sni = 'off';
} else if (!['tuic'].includes(proxy.type)) {
$.error(
`Target platform ${targetPlatform} does not support sni off. Proxy's fields (sni, tls-fingerprint and skip-cert-verify) will be modified.`,
);
proxy.sni = '';
proxy['skip-cert-verify'] = true;
delete proxy['tls-fingerprint'];
}
}
// 处理 端口跳跃
if (proxy.ports) {
proxy.ports = String(proxy.ports);
if (!['ClashMeta'].includes(targetPlatform)) {
proxy.ports = proxy.ports.replace(/\//g, ',');
}
if (!proxy.port) {
proxy.port = getRandomPort(proxy.ports);
}
}
return proxy;
});
$.info(`Producing proxies for target: ${targetPlatform}`);
$.log(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
const list = proxies
let list = proxies
.map((proxy) => {
try {
let line = producer.produce(proxy, type, opts);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
return producer.produce(proxy, type, opts);
} catch (err) {
$.error(
`Cannot produce proxy: ${JSON.stringify(
@@ -222,7 +310,18 @@ function produce(proxies, targetPlatform, type, opts = {}) {
}
})
.filter((line) => line.length > 0);
return type === 'internal' ? list : list.join('\n');
list = type === 'internal' ? list : list.join('\n');
if (
targetPlatform.startsWith('Surge') &&
proxies.length > 0 &&
proxies.every((p) => p.type === 'wireguard')
) {
list = `#!name=${proxies[0]?._subName}
#!desc=${proxies[0]?._desc ?? ''}
#!category=${proxies[0]?._category ?? ''}
${list}`;
}
return list;
} else if (producer.type === 'ALL') {
return producer.produce(proxies, type, opts);
}
@@ -232,10 +331,24 @@ export const ProxyUtils = {
parse,
process: processFn,
produce,
ipAddress,
getRandomPort,
isIPv4,
isIPv6,
isIP,
yaml: YAML,
getFlag,
removeFlag,
getISO,
MMDB,
Gist,
download,
downloadFile,
isValidUUID,
doh,
Buffer,
Base64,
JSON5,
};
function tryParse(parser, line) {
@@ -256,7 +369,38 @@ function safeMatch(parser, line) {
}
}
function formatTransportPath(path) {
if (typeof path === 'string' || typeof path === 'number') {
path = String(path).trim();
if (path === '') {
return '/';
} else if (!path.startsWith('/')) {
return '/' + path;
}
}
return path;
}
function lastParse(proxy) {
if (typeof proxy.cipher === 'string') {
proxy.cipher = proxy.cipher.toLowerCase();
}
if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password);
}
if (
['ss'].includes(proxy.type) &&
proxy.cipher === 'none' &&
!proxy.password
) {
// https://github.com/MetaCubeX/mihomo/issues/1677
proxy.password = '';
}
if (proxy.interface) {
proxy['interface-name'] = proxy.interface;
delete proxy.interface;
}
if (isValidPortNumber(proxy.port)) {
proxy.port = parseInt(proxy.port, 10);
}
@@ -280,6 +424,17 @@ function lastParse(proxy) {
delete proxy['ws-headers'];
}
const transportPath = proxy[`${proxy.network}-opts`]?.path;
if (Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = transportPath.map((item) =>
formatTransportPath(item),
);
} else if (transportPath != null) {
proxy[`${proxy.network}-opts`].path =
formatTransportPath(transportPath);
}
if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') {
delete proxy.network;
@@ -290,7 +445,16 @@ function lastParse(proxy) {
proxy.network = 'tcp';
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
if (
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
proxy.tls = true;
}
if (proxy.network) {
@@ -316,20 +480,7 @@ function lastParse(proxy) {
proxy['h2-opts'].path = path[0];
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
if (transportHost) {
proxy.sni = transportHost;
}
}
if (!proxy.sni && !isIP(proxy.server)) {
proxy.sni = proxy.server;
}
}
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
if (
!proxy.tls &&
@@ -356,10 +507,63 @@ function lastParse(proxy) {
proxy[`${proxy.network}-opts`].path = [transportPath];
}
}
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
if (proxy.tls && !proxy.sni) {
if (!isIP(proxy.server)) {
proxy.sni = proxy.server;
}
if (!proxy.sni && proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
if (transportHost) {
proxy.sni = transportHost;
}
}
}
// if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) {
if (proxy.ports) {
proxy.ports = String(proxy.ports).replace(/\//g, ',');
} else {
delete proxy.ports;
}
// }
if (
['hysteria2'].includes(proxy.type) &&
proxy.obfs &&
!['salamander'].includes(proxy.obfs) &&
!proxy['obfs-password']
) {
proxy['obfs-password'] = proxy.obfs;
proxy.obfs = 'salamander';
}
if (
['hysteria2'].includes(proxy.type) &&
!proxy['obfs-password'] &&
proxy['obfs_password']
) {
proxy['obfs-password'] = proxy['obfs_password'];
delete proxy['obfs_password'];
}
if (['vless'].includes(proxy.type)) {
// 删除 reality-opts: {}
if (
proxy['reality-opts'] &&
Object.keys(proxy['reality-opts']).length === 0
) {
delete proxy['reality-opts'];
}
// 删除 grpc-opts: {}
if (
proxy['grpc-opts'] &&
Object.keys(proxy['grpc-opts']).length === 0
) {
delete proxy['grpc-opts'];
}
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
}
if (['http'].includes(proxy.network)) {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
if (!transportPath) {
@@ -370,6 +574,77 @@ function lastParse(proxy) {
}
}
}
if (typeof proxy.name !== 'string') {
if (/^\d+$/.test(proxy.name)) {
proxy.name = `${proxy.name}`;
} else {
try {
if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else {
proxy.name = Buffer.from(proxy.name).toString('utf8');
}
} catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`);
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
}
}
if (['ws', 'http', 'h2'].includes(proxy.network)) {
if (
['ws', 'h2'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.path
) {
proxy[`${proxy.network}-opts`] =
proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].path = '/';
} else if (
proxy.network === 'http' &&
(!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||
proxy[`${proxy.network}-opts`]?.path.every((i) => !i))
) {
proxy[`${proxy.network}-opts`] =
proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].path = ['/'];
}
}
if (['', 'off'].includes(proxy.sni)) {
proxy['disable-sni'] = true;
}
let caStr = proxy['ca_str'];
if (proxy['ca-str']) {
caStr = proxy['ca-str'];
} else if (caStr) {
delete proxy['ca_str'];
proxy['ca-str'] = caStr;
}
try {
if ($.env.isNode && !caStr && proxy['_ca']) {
caStr = $.node.fs.readFileSync(proxy['_ca'], {
encoding: 'utf8',
});
}
} catch (e) {
$.error(`Read ca file failed\nReason: ${e}`);
}
if (!proxy['tls-fingerprint'] && caStr) {
proxy['tls-fingerprint'] = rs.generateFingerprint(caStr);
}
if (
['ss'].includes(proxy.type) &&
isPresent(proxy, 'shadow-tls-password')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-version'];
}
return proxy;
}

View File

@@ -5,17 +5,122 @@ import {
isPresent,
isNotBlank,
getIfPresent,
getRandomPort,
} from '@/utils';
import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx';
import getTrojanURIParser from './peggy/trojan-uri';
import $ from '@/core/app';
import JSON5 from 'json5';
import { Base64 } from 'js-base64';
function surge_port_hopping(raw) {
const [parts, port_hopping] =
raw.match(
/,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
) || [];
return {
port_hopping: port_hopping
? port_hopping.replace(/;/g, ',')
: undefined,
line: parts ? raw.replace(parts, '') : raw,
};
}
function URI_PROXY() {
// socks5+tls
// socks5
// http, https(可以这么写)
const name = 'URI PROXY Parser';
const test = (line) => {
return /^(socks5\+tls|socks5|http|https):\/\//.test(line);
};
const parse = (line) => {
// parse url
// eslint-disable-next-line no-unused-vars
let [__, type, tls, username, password, server, port, query, name] =
line.match(
/^(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 = {
name:
name != null
? decodeURIComponent(name)
: `${type} ${server}:${port}`,
type,
tls: tls ? true : false,
server,
port,
username:
username != null ? decodeURIComponent(username) : undefined,
password:
password != null ? decodeURIComponent(password) : undefined,
};
return proxy;
};
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).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() {
// TODO: 暂不支持 httpupgrade
const name = 'URI SS Parser';
const test = (line) => {
return /^ss:\/\//.test(line);
@@ -24,14 +129,22 @@ function URI_SS() {
// parse url
let content = line.split('ss://')[1];
let name = line.split('#')[1];
const proxy = {
name: decodeURIComponent(line.split('#')[1]),
type: 'ss',
};
content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]);
let serverAndPortArray = content.match(/@([^/?]*)(\/|\?|$)/);
let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大
let userInfoStr;
if (rawUserInfoStr?.startsWith('2022-blake3-')) {
userInfoStr = rawUserInfoStr;
} else {
userInfoStr = Base64.decode(rawUserInfoStr);
}
let query = '';
if (!serverAndPortArray) {
if (content.includes('?')) {
@@ -40,6 +153,7 @@ function URI_SS() {
query = parsed[2];
}
content = Base64.decode(content);
if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
@@ -53,26 +167,35 @@ function URI_SS() {
}
content = `${content}${query}`;
}
userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
userInfoStr = content.match(/(^.*)@/)?.[1];
serverAndPortArray = content.match(/@([^/@]*)(\/|$)/);
} else if (content.includes('?')) {
const parsed = content.match(/(\?.*)$/);
query = parsed[1];
}
const serverAndPort = serverAndPortArray[1];
const portIdx = serverAndPort.lastIndexOf(':');
proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
/\d+/,
)?.[0];
const userInfo = userInfoStr.split(':');
proxy.cipher = userInfo[0];
proxy.password = userInfo[1];
let userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = userInfo?.[1];
proxy.password = userInfo?.[2];
// if (!proxy.cipher || !proxy.password) {
// userInfo = rawUserInfoStr.match(/(^.*?):(.*$)/);
// proxy.cipher = userInfo?.[1];
// proxy.password = userInfo?.[2];
// }
// handle obfs
const idx = content.indexOf('?plugin=');
if (idx !== -1) {
const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
if (pluginMatch) {
const pluginInfo = (
'plugin=' +
decodeURIComponent(content.split('?plugin=')[1].split('&')[0])
'plugin=' + decodeURIComponent(pluginMatch[1])
).split(';');
const params = {};
for (const item of pluginInfo) {
@@ -97,18 +220,51 @@ function URI_SS() {
tls: getIfPresent(params.tls),
};
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:
throw new Error(
`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)) {
proxy['udp-over-tcp'] = true;
}
if (/(&|\?)tfo=(1|true)/i.test(query)) {
proxy.tfo = true;
}
if (name != null) {
name = decodeURIComponent(name);
}
proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`;
return proxy;
};
return { name, test, parse };
@@ -157,7 +313,7 @@ function URI_SSR() {
for (const item of line) {
let [key, val] = item.split('=');
val = val.trim();
if (val.length > 0) {
if (val.length > 0 && val !== '(null)') {
other_params[key] = val;
}
}
@@ -191,7 +347,7 @@ function URI_VMess() {
};
const parse = (line) => {
line = line.split('vmess://')[1];
let content = Base64.decode(line);
let content = Base64.decode(line.replace(/\?.*?$/, ''));
if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format
const partitions = content.split(',').map((p) => p.trim());
@@ -284,7 +440,16 @@ function URI_VMess() {
type: 'vmess',
server,
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,
alterId: parseInt(
getIfPresent(params.aid ?? params.alterId, 0),
@@ -295,21 +460,44 @@ function URI_VMess() {
? !params.verify_cert
: undefined,
};
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
proxy.sni = params.sni;
if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(
params.allowInsecure,
);
}
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls) {
if (params.sni && params.sni !== '') {
proxy.sni = params.sni;
} else if (params.peer && params.peer !== '') {
proxy.sni = params.peer;
}
}
let httpupgrade = false;
// handle obfs
if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws';
} else if (
['tcp', 'http'].includes(params.net) ||
params.obfs === 'http'
['http'].includes(params.net) ||
['http'].includes(params.obfs) ||
['http'].includes(params.type)
) {
proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
} else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
proxy.network = params.net;
} else if (
params.net === 'httpupgrade' ||
proxy.network === 'httpupgrade'
) {
proxy.network = 'ws';
httpupgrade = true;
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
}
// 暂不支持 tcp + host + path
// else if (params.net === 'tcp' || proxy.network === 'tcp') {
// proxy.network = 'tcp';
// }
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
try {
@@ -322,8 +510,17 @@ function URI_VMess() {
} catch (e) {}
let transportPath = params.path;
// 补上默认 path
if (['ws'].includes(proxy.network)) {
transportPath = transportPath || '/';
}
if (proxy.network === 'http') {
if (transportHost) {
// 1)http(tcp)->host中间逗号(,)隔开
transportHost = transportHost
.split(',')
.map((i) => i.trim());
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
@@ -332,30 +529,54 @@ function URI_VMess() {
transportPath = Array.isArray(transportPath)
? transportPath[0]
: transportPath;
} else {
transportPath = '/';
}
}
if (transportPath || transportHost) {
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
if (
transportPath ||
transportHost ||
['kcp', 'quic'].includes(proxy.network)
) {
if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type),
'_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 {
proxy[`${proxy.network}-opts`] = {
const opts = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
if (httpupgrade) {
opts['v2ray-http-upgrade'] = true;
opts['v2ray-http-upgrade-fast-open'] = true;
}
proxy[`${proxy.network}-opts`] = opts;
}
} else {
delete proxy.network;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
// sni 优先级应高于 host
if (proxy.tls && !proxy.sni && transportHost) {
proxy.sni = transportHost;
}
}
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;
}
};
@@ -439,17 +660,27 @@ function URI_VLESS() {
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (Object.keys(opts).length > 0) {
// proxy[`${params.security}-opts`] = opts;
proxy[`${params.security}-opts`] = opts;
}
}
let httpupgrade = false;
proxy.network = params.type;
if (proxy.network === 'tcp' && params.headerType === 'http') {
proxy.network = 'http';
} else if (proxy.network === 'httpupgrade') {
proxy.network = 'ws';
httpupgrade = true;
}
if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs;
if (['none'].includes(proxy.network)) {
proxy.network = 'tcp';
}
}
if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws';
@@ -471,6 +702,9 @@ function URI_VLESS() {
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
if (['grpc'].includes(proxy.network) && params.authority) {
opts['_grpc-authority'] = params.authority;
}
} else if (isShadowrocket && params.path) {
if (!['ws', 'http', 'h2'].includes(proxy.network)) {
opts[`${proxy.network}-service-name`] = params.path;
@@ -484,17 +718,75 @@ function URI_VLESS() {
if (['grpc'].includes(proxy.network)) {
opts['_grpc-type'] = params.mode || 'gun';
}
if (httpupgrade) {
opts['v2ray-http-upgrade'] = true;
opts['v2ray-http-upgrade-fast-open'] = true;
}
if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts;
}
if (proxy.network === 'kcp') {
// mKCP 种子。省略时不使用种子,但不可以为空字符串。建议 mKCP 用户使用 seed。
if (params.seed) {
proxy.seed = params.seed;
}
// mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none即不使用伪装头部但不可以为空字符串。
proxy.headerType = params.headerType || 'none';
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
return proxy;
};
return { name, test, parse };
}
function URI_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;
}
}
@@ -509,13 +801,43 @@ function URI_Hysteria2() {
};
const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, ____, addons = '', name] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
// 端口跳跃有两种写法:
// 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
// 端口部分支持 端口跳跃 的「多端口地址格式」。
// https://hysteria.network/zh/docs/advanced/Port-Hopping
// 2. 参数 mport
let ports;
/* eslint-disable no-unused-vars */
let [
__,
password,
server,
___,
port,
____,
_____,
______,
_______,
________,
addons = '',
name,
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
line,
);
/* eslint-enable no-unused-vars */
if (/^\d+$/.test(port)) {
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
} else if (port) {
ports = port;
port = getRandomPort(ports);
} else {
port = 443;
}
password = decodeURIComponent(password);
if (name != null) {
name = decodeURIComponent(name);
@@ -527,6 +849,7 @@ function URI_Hysteria2() {
name,
server,
port,
ports,
password,
};
@@ -545,11 +868,23 @@ function URI_Hysteria2() {
if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs;
}
if (params.mport) {
proxy.ports = params.mport;
}
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256;
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;
};
@@ -632,8 +967,11 @@ function URI_TUIC() {
const parse = (line) => {
line = line.split(/tuic:\/\//)[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, password, server, ___, port, ____, addons = '', name] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
let [__, auth, server, port, addons = '', name] =
/^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
auth = decodeURIComponent(auth);
let [uuid, ...passwordParts] = auth.split(':');
let password = passwordParts.join(':');
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
@@ -655,14 +993,19 @@ function URI_TUIC() {
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/, '-');
key = key.replace(/_/g, '-');
value = decodeURIComponent(value);
if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined;
} else if (['allow-insecure'].includes(key)) {
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)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else if (key === 'congestion-control') {
proxy['congestion-controller'] = value;
delete proxy[key];
} else {
proxy[key] = value;
}
@@ -672,6 +1015,89 @@ function URI_TUIC() {
};
return { name, test, parse };
}
function URI_WireGuard() {
const name = 'URI WireGuard Parser';
const test = (line) => {
return /^(wireguard|wg):\/\//.test(line);
};
const parse = (line) => {
line = line.split(/(wireguard|wg):\/\//)[2];
/* eslint-disable no-unused-vars */
let [
__,
___,
privateKey,
server,
____,
port,
_____,
addons = '',
name,
] = /^((.*?)@)?(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
/* eslint-enable no-unused-vars */
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 51820;
}
privateKey = decodeURIComponent(privateKey);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `WireGuard ${server}:${port}`;
const proxy = {
type: 'wireguard',
name,
server,
port,
'private-key': privateKey,
udp: true,
};
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/, '-');
value = decodeURIComponent(value);
if (['reserved'].includes(key)) {
const parsed = value
.split(',')
.map((i) => parseInt(i.trim(), 10))
.filter((i) => Number.isInteger(i));
if (parsed.length === 3) {
proxy[key] = parsed;
}
} else if (['address', 'ip'].includes(key)) {
value.split(',').map((i) => {
const ip = i
.trim()
.replace(/\/\d+$/, '')
.replace(/^\[/, '')
.replace(/\]$/, '');
if (isIPv4(ip)) {
proxy.ip = ip;
} else if (isIPv6(ip)) {
proxy.ipv6 = ip;
}
});
} else if (['mtu'].includes(key)) {
const parsed = parseInt(value.trim(), 10);
if (Number.isInteger(parsed)) {
proxy[key] = parsed;
}
} else if (/publickey/i.test(key)) {
proxy['public-key'] = value;
} else if (/privatekey/i.test(key)) {
proxy['private-key'] = value;
} else if (['udp'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else if (!['flag'].includes(key)) {
proxy[key] = value;
}
}
return proxy;
};
return { name, test, parse };
}
// Trojan URI format
function URI_Trojan() {
@@ -681,6 +1107,11 @@ function URI_Trojan() {
};
const parse = (line) => {
const matched = /^(trojan:\/\/.*?@.*?)(:(\d+))?\/?(\?.*?)?$/.exec(line);
const port = matched?.[2];
if (!port) {
line = line.replace(matched[1], `${matched[1]}:443`);
}
let [newLine, name] = line.split(/#(.+)/, 2);
const parser = getTrojanURIParser();
const proxy = parser.parse(newLine);
@@ -700,16 +1131,19 @@ function Clash_All() {
const name = 'Clash Parser';
const test = (line) => {
try {
JSON.parse(line);
JSON5.parse(line);
} catch (e) {
return false;
}
return true;
};
const parse = (line) => {
const proxy = JSON.parse(line);
const proxy = JSON5.parse(line);
if (
![
'anytls',
'mieru',
'juicity',
'ss',
'ssr',
'vmess',
@@ -722,6 +1156,8 @@ function Clash_All() {
'hysteria',
'hysteria2',
'wireguard',
'ssh',
'direct',
].includes(proxy.type)
) {
throw new Error(
@@ -733,21 +1169,16 @@ function Clash_All() {
if (['vmess', 'vless'].includes(proxy.type)) {
proxy.sni = proxy.servername;
delete proxy.servername;
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost)
? httpHost[0]
: httpHost;
}
}
}
if (proxy['server-cert-fingerprint']) {
proxy['tls-fingerprint'] = proxy['server-cert-fingerprint'];
}
if (proxy.fingerprint) {
proxy['tls-fingerprint'] = proxy.fingerprint;
}
if (proxy['dialer-proxy']) {
proxy['underlying-proxy'] = proxy['dialer-proxy'];
}
if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url'];
@@ -907,6 +1338,15 @@ function Loon_Http() {
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_Socks5() {
const name = 'Loon SOCKS5 Parser';
const test = (line) => {
return /^.*=\s*socks5/i.test(line.split(',')[0]);
};
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_WireGuard() {
const name = 'Loon WireGuard Parser';
@@ -1016,6 +1456,14 @@ function Loon_WireGuard() {
return { name, test, parse };
}
function Surge_Direct() {
const name = 'Surge Direct Parser';
const test = (line) => {
return /^.*=\s*direct/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_SSH() {
const name = 'Surge SSH Parser';
const test = (line) => {
@@ -1159,7 +1607,12 @@ function Surge_Tuic() {
const test = (line) => {
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
return { name, test, parse };
}
function Surge_WireGuard() {
@@ -1176,7 +1629,12 @@ function Surge_Hysteria2() {
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
return { name, test, parse };
}
@@ -1185,15 +1643,20 @@ function isIP(ip) {
}
export default [
URI_PROXY(),
URI_SOCKS(),
URI_SS(),
URI_SSR(),
URI_VMess(),
URI_VLESS(),
URI_TUIC(),
URI_WireGuard(),
URI_Hysteria(),
URI_Hysteria2(),
URI_Trojan(),
URI_AnyTLS(),
Clash_All(),
Surge_Direct(),
Surge_SSH(),
Surge_SS(),
Surge_VMess(),
@@ -1212,6 +1675,7 @@ export default [
Loon_Hysteria2(),
Loon_Trojan(),
Loon_Http(),
Loon_Socks5(),
Loon_WireGuard(),
QX_SS(),
QX_SSR(),

View File

@@ -35,16 +35,16 @@ const grammars = String.raw`
}
}
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
return proxy;
}
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";
// handle ssr obfs
proxy.obfs = obfs.type;
}
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/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";
// handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -54,30 +54,33 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path);
}
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
proxy.type = "vless";
handleTransport();
}
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "trojan";
handleTransport();
}
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* {
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http";
proxy.tls = true;
}
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "socks5";
}
address = comma server:server comma port:port {
proxy.server = server;
@@ -117,7 +120,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
username = & {
let j = peg$currPos;
@@ -166,15 +169,30 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _

View File

@@ -33,16 +33,16 @@
}
}
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
return proxy;
}
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";
// handle ssr obfs
proxy.obfs = obfs.type;
}
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/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";
// handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -52,30 +52,33 @@ shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs
$set(proxy, "plugin-opts.path", obfs.path);
}
}
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/vmess_alterId/fast_open/udp_relay/others)* {
vmess = tag equals "vmess"i address method uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/vmess_alterId/fast_open/udp_relay/ip_mode/public_key/short_id/block_quic/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
proxy.alterId = proxy.alterId || 0;
handleTransport();
}
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
vless = tag equals "vless"i address uuid (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/flow/public_key/short_id/block_quic/others)* {
proxy.type = "vless";
handleTransport();
}
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
trojan = tag equals "trojan"i address password (transport/transport_host/transport_path/over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "trojan";
handleTransport();
}
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* {
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/udp_relay/fast_open/download_bandwidth/salamander_password/ecn/ip_mode/block_quic/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
https = tag equals "https"i address (username password)? (tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http";
proxy.tls = true;
}
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
http = tag equals "http"i address (username password)? (fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/tls_cert_sha256/tls_pubkey_sha256/fast_open/udp_relay/ip_mode/block_quic/others)* {
proxy.type = "socks5";
}
address = comma server:server comma port:port {
proxy.server = server;
@@ -115,7 +118,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"auto"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"rc4-md5"/"rc4"/"salsa20"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
username = & {
let j = peg$currPos;
@@ -164,15 +167,30 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
over_tls = comma "over-tls" equals flag:bool { proxy.tls = flag; }
tls_host = comma "tls-name" equals host:domain { proxy.sni = host; }
tls_host = comma sni:("tls-name"/"sni") equals host:domain { proxy.sni = host; }
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_cert_sha256 = comma "tls-cert-sha256" equals match:[^,]+ { proxy["tls-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals match:[^,]+ { proxy["tls-pubkey-sha256"] = match.join("").replace(/^"(.*)"$/, '$1'); }
flow = comma "flow" equals match:[^,]+ { proxy["flow"] = match.join("").replace(/^"(.*)"$/, '$1'); }
public_key = comma "public-key" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["public-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
short_id = comma "short-id" equals match:[^,]+ { proxy["reality-opts"] = proxy["reality-opts"] || {}; proxy["reality-opts"]["short-id"] = match.join("").replace(/^"(.*)"$/, '$1'); }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ip_mode = comma "ip-mode" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
block_quic = comma "block-quic" equals flag:bool { if(flag) proxy["block-quic"] = "on"; else proxy["block-quic"] = "off"; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _

View File

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

View File

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

View File

@@ -37,11 +37,11 @@ const grammars = String.raw`
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/direct) {
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -52,36 +52,37 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
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.cipher = proxy.cipher || "none";
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
if (proxy.aead) {
proxy.alterId = 0;
} else {
proxy.alterId = proxy.alterId || 0;
proxy.alterId = 1;
}
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/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";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/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.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/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";
handleShadowTLS();
}
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/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";
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";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -91,11 +92,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
}
handleShadowTLS();
}
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic";
handleShadowTLS();
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic";
proxy.version = 5;
handleShadowTLS();
@@ -104,20 +105,22 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (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";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (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.tls = true;
handleShadowTLS();
}
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {
proxy.server = server;
proxy.port = port;
@@ -151,6 +154,8 @@ port = digits:[0-9]+ {
}
}
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
username = & {
let j = peg$currPos;
let start, end;
@@ -173,26 +178,32 @@ username = & {
peg$currPos = end;
return true;
}
} { proxy.username = $.username; }
password = comma match:[^,]+ { proxy.password = match.join(""); }
} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:domain { proxy.sni = sni; }
sni = comma "sni" equals sni:("off"/domain) {
if (sni === "off") {
proxy["disable-sni"] = true;
} else {
proxy.sni = sni;
}
}
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
@@ -200,18 +211,18 @@ ws_headers = comma "ws-headers" equals headers:$[^,]+ {
const result = {};
pairs.forEach(pair => {
const [key, value] = pair.trim().split(":");
result[key.trim()] = value.trim();
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
})
obfs["ws-headers"] = result;
}
ws_path = comma "ws-path" equals path:uri { obfs.path = path; }
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
@@ -229,11 +240,13 @@ interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -35,11 +35,11 @@
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/direct) {
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/udp_port/others)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -50,36 +50,37 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
}
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.cipher = proxy.cipher || "none";
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
if (proxy.aead) {
proxy.alterId = 0;
} else {
proxy.alterId = proxy.alterId || 0;
proxy.alterId = 1;
}
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/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";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/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.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (usernamek passwordk)? (ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/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";
handleShadowTLS();
}
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/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";
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";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -89,11 +90,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
}
handleShadowTLS();
}
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic";
handleShadowTLS();
}
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic";
proxy.version = 5;
handleShadowTLS();
@@ -102,20 +103,22 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (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";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (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.tls = true;
handleShadowTLS();
}
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {
proxy.server = server;
proxy.port = port;
@@ -149,6 +152,8 @@ port = digits:[0-9]+ {
}
}
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
username = & {
let j = peg$currPos;
let start, end;
@@ -171,26 +176,32 @@ username = & {
peg$currPos = end;
return true;
}
} { proxy.username = $.username; }
password = comma match:[^,]+ { proxy.password = match.join(""); }
} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:domain { proxy.sni = sni; }
sni = comma "sni" equals sni:("off"/domain) {
if (sni === "off") {
proxy["disable-sni"] = true;
} else {
proxy.sni = sni;
}
}
tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-verify"] = flag; }
tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join(""); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305");
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
@@ -198,18 +209,18 @@ ws_headers = comma "ws-headers" equals headers:$[^,]+ {
const result = {};
pairs.forEach(pair => {
const [key, value] = pair.trim().split(":");
result[key.trim()] = value.trim();
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
})
obfs["ws-headers"] = result;
}
ws_path = comma "ws-path" equals path:uri { obfs.path = path; }
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };
obfs_uri = comma "obfs-uri" equals path:uri { obfs.path = path }
uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
@@ -227,11 +238,13 @@ interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
udp_port = comma "udp-port" equals match:$[0-9]+ { proxy["udp-port"] = parseInt(match.trim()); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -80,8 +80,13 @@ port = digits:[0-9]+ {
}
params = "?" head:param tail:("&"@param)* {
for (const [key, value] of Object.entries(params)) {
params[key] = decodeURIComponent(value);
}
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
if (toBool(params["ws"])) {
proxy.network = "ws";
@@ -89,11 +94,17 @@ params = "?" head:param tail:("&"@param)* {
}
if (params["type"]) {
let httpupgrade
proxy.network = params["type"]
if(proxy.network === 'httpupgrade') {
proxy.network = 'ws'
httpupgrade = true
}
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
'_grpc-authority': params["authority"],
};
} else {
if (params["path"]) {
@@ -102,6 +113,31 @@ params = "?" head:param tail:("&"@param)* {
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
if (httpupgrade) {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
if (Object.keys(opts).length > 0) {
$set(proxy, params.security+"-opts", opts);
}
}
}

View File

@@ -78,8 +78,13 @@ port = digits:[0-9]+ {
}
params = "?" head:param tail:("&"@param)* {
for (const [key, value] of Object.entries(params)) {
params[key] = decodeURIComponent(value);
}
proxy["skip-cert-verify"] = toBool(params["allowInsecure"]);
proxy.sni = params["sni"] || params["peer"];
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? decodeURIComponent(params.alpn).split(',') : undefined;
if (toBool(params["ws"])) {
proxy.network = "ws";
@@ -87,11 +92,17 @@ params = "?" head:param tail:("&"@param)* {
}
if (params["type"]) {
let httpupgrade
proxy.network = params["type"]
if(proxy.network === 'httpupgrade') {
proxy.network = 'ws'
httpupgrade = true
}
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
'_grpc-authority': params["authority"],
};
} else {
if (params["path"]) {
@@ -100,6 +111,31 @@ params = "?" head:param tail:("&"@param)* {
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
if (httpupgrade) {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
if (Object.keys(opts).length > 0) {
$set(proxy, params.security+"-opts", opts);
}
}
}

View File

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

View File

@@ -1,13 +1,16 @@
import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { isIPv4, isIPv6, ipAddress } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo';
import { getFlag, removeFlag } from '@/utils/geo';
import { doh } from '@/utils/dns';
import lodash from 'lodash';
import $ from '@/core/app';
import { hex_md5 } from '@/vendor/md5';
import { ProxyUtils } from '@/core/proxy-utils';
import { produceArtifact } from '@/restful/sync';
import { SETTINGS_KEY } from '@/constants';
import YAML from '@/utils/yaml';
import env from '@/utils/env';
import {
@@ -16,8 +19,50 @@ import {
parseFlowHeaders,
validCheck,
flowTransfer,
getRmainingDays,
normalizeFlowHeader,
} from '@/utils/flow';
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
function trimWrap(str) {
if (str.startsWith('<') && str.endsWith('>')) {
return str.slice(1, -1);
}
return str;
}
function deepMerge(target, _other) {
const other = typeof _other === 'string' ? JSON.parse(_other) : _other;
for (const key in other) {
if (isObject(other[key])) {
if (key.endsWith('!')) {
const k = trimWrap(key.slice(0, -1));
target[k] = other[key];
} else {
const k = trimWrap(key);
if (!target[k]) Object.assign(target, { [k]: {} });
deepMerge(target[k], other[k]);
}
} else if (Array.isArray(other[key])) {
if (key.startsWith('+')) {
const k = trimWrap(key.slice(1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...other[key], ...target[k]];
} else if (key.endsWith('+')) {
const k = trimWrap(key.slice(0, -1));
if (!target[k]) Object.assign(target, { [k]: [] });
target[k] = [...target[k], ...other[key]];
} else {
const k = trimWrap(key);
Object.assign(target, { [k]: other[key] });
}
} else {
Object.assign(target, { [key]: other[key] });
}
}
return target;
}
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
{
@@ -239,7 +284,15 @@ function SortOperator(order = 'asc') {
}
// 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 {
name: 'Regex Sort Operator',
func: (proxies) => {
@@ -250,8 +303,13 @@ function RegexSortOperator(expressions) {
if (oA && !oB) return -1;
if (oB && !oA) return 1;
if (oA && oB) return oA < oB ? -1 : 1;
if ((!oA && !oB) || (oA && oB && oA === oB))
return a.name < b.name ? -1 : 1; // fallback to normal sort
if (order === 'original') {
return 0;
} else if (order === 'desc') {
return a.name < b.name ? 1 : -1;
} else {
return a.name < b.name ? -1 : 1;
}
});
},
};
@@ -313,16 +371,57 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`!
2. Always declare variables before using them!
*/
function ScriptOperator(script, targetPlatform, $arguments, source) {
function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
return {
name: 'Script Operator',
func: async (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 () {
const operator = createDynamicFunction(
'operator',
script,
$arguments,
$options,
);
output = operator(proxies, targetPlatform, { source, ...env });
})();
@@ -335,9 +434,34 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
'operator',
`async function operator(input = []) {
if (input && (input.$files || input.$content)) {
let { $content, $files } = input
${script}
return { $content, $files }
let { $content, $files, $options, $file } = input
if($file.type === 'mihomoProfile') {
${script}
if(typeof main === 'function') {
let config;
if ($content) {
try {
config = ProxyUtils.yaml.safeLoad($content);
} catch (e) {
console.log(e.message ?? e);
}
}
$content = ProxyUtils.yaml.safeDump(await main(config || ($file.sourceType === 'none' ? {} : {
proxies: await produceArtifact({
type: $file.sourceType || 'collection',
name: $file.sourceName,
platform: 'mihomo',
produceType: 'internal',
produceOpts: {
'delete-underscore-fields': true
}
}),
})))
}
} else {
${script}
}
return { $content, $files, $options, $file }
} else {
let proxies = input
let list = []
@@ -349,6 +473,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
}
}`,
$arguments,
$options,
);
output = operator(proxies, targetPlatform, { source, ...env });
})();
@@ -357,131 +482,233 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
};
}
const DOMAIN_RESOLVERS = {
Google: async function (domain, type) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
headers: {
accept: 'application/dns-json',
},
});
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) {
throw new Error(`Status is ${body['Status']}`);
function parseIP4P(IP4P) {
let server;
let port;
try {
let array = IP4P.split(':');
port = parseInt(array[2], 16);
let ipab = parseInt(array[3], 16);
let ipcd = parseInt(array[4], 16);
let ipa = ipab >> 8;
let ipb = ipab & 0xff;
let ipc = ipcd >> 8;
let ipd = ipcd & 0xff;
server = `${ipa}.${ipb}.${ipc}.${ipd}`;
if (port <= 0 || port > 65535) {
throw new Error(`Invalid port number: ${port}`);
}
const answers = body['Answer'];
if (answers.length === 0) {
if (!isIPv4(server)) {
throw new Error(`Invalid IP address: ${server}`);
}
} catch (e) {
// throw new Error(`IP4P 解析失败: ${e}`);
$.error(`IP4P 解析失败: ${e}`);
}
return { server, port };
}
const DOMAIN_RESOLVERS = {
Custom: async function (domain, type, noCache, timeout, edns, url) {
const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
const res = await doh({
url,
domain,
type: answerType,
timeout,
edns,
});
const { answers } = res;
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain) {
Google: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
const res = await doh({
url: 'https://8.8.4.4/dns-query',
domain,
type: answerType,
timeout,
edns,
});
const { answers } = res;
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain, type, noCache, timeout) {
if (['IPv6'].includes(type)) {
throw new Error(`域名解析服务提供方 IP-API 不支持 ${type}`);
}
const id = hex_md5(`IP-API:${domain}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://ip-api.com/json/${encodeURIComponent(
domain,
)}?lang=zh-CN`,
timeout,
});
const body = JSON.parse(resp.body);
if (body['status'] !== 'success') {
throw new Error(`Status is ${body['status']}`);
}
const result = body.query;
if (!body.query || body.query === 0) {
throw new Error('No answers');
}
const result = [body.query];
if (result.length === 0) {
throw new Error('No answers');
}
resourceCache.set(id, result);
return result;
},
Cloudflare: async function (domain, type) {
Cloudflare: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
headers: {
accept: 'application/dns-json',
},
if (!noCache && cached) return cached;
const answerType = type === 'IPv6' ? 'AAAA' : 'A';
const res = await doh({
url: 'https://1.0.0.1/dns-query',
domain,
type: answerType,
timeout,
edns,
});
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) {
throw new Error(`Status is ${body['Status']}`);
}
const answers = body['Answer'];
if (answers.length === 0) {
const { answers } = res;
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1].data;
resourceCache.set(id, result);
return result;
},
Ali: async function (domain, type) {
Ali: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
url: `http://223.6.6.6/resolve?edns_client_subnet=${edns}/${
isIPv4(edns) ? 24 : 56
}&name=${encodeURIComponent(domain)}&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&short=1`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const answers = JSON.parse(resp.body);
if (answers.length === 0) {
if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers');
}
const result = answers;
if (result.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain, type) {
const id = hex_md5(`ALI:${domain}:${type}`);
Tencent: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`TENCENT:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?type=${
url: `http://119.28.28.28/d?ip=${edns}&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&dn=${encodeURIComponent(domain)}`,
headers: {
accept: 'application/dns-json',
},
timeout,
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0) {
if (answers.length === 0 || String(answers) === '0') {
throw new Error('No answers');
}
const result = answers;
if (result.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
};
function ResolveDomainOperator({ provider, type, filter }) {
if (type === 'IPv6' && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 IPv6`);
function ResolveDomainOperator({
provider,
type: _type,
filter,
cache,
url,
timeout,
edns: _edns,
}) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
const { defaultTimeout } = $.read(SETTINGS_KEY);
const requestTimeout = timeout || defaultTimeout || 8000;
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) {
throw new Error(`找不到域名解析服务提供方: ${provider}`);
}
let edns = _edns || '223.6.6.6';
if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
$.info(
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,
);
return {
name: 'Resolve Domain Operator',
func: async (proxies) => {
proxies.forEach((p, i) => {
if (!p['_no-resolve'] && p['no-resolve']) {
proxies[i]['_no-resolve'] = p['no-resolve'];
}
});
const results = {};
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
const totalDomain = [
...new Set(
proxies
.filter((p) => !isIP(p.server) && !p['no-resolve'])
.filter((p) => !isIP(p.server) && !p['_no-resolve'])
.map((c) => c.server),
),
];
@@ -490,7 +717,14 @@ function ResolveDomainOperator({ provider, type, filter }) {
const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push(
resolver(domain, type)
resolver(
domain,
type,
cache === 'disabled',
requestTimeout,
edns,
url,
)
.then((ip) => {
results[domain] = ip;
$.info(
@@ -507,11 +741,58 @@ function ResolveDomainOperator({ provider, type, filter }) {
await Promise.all(currentBatch);
}
proxies.forEach((p) => {
if (!p['no-resolve']) {
if (!p['_no-resolve']) {
if (results[p.server]) {
p.server = results[p.server];
p.resolved = true;
} else {
p._resolved_ips = results[p.server];
let ip = Array.isArray(results[p.server])
? results[p.server][
Math.floor(
Math.random() * results[p.server].length,
)
]
: results[p.server];
if (type === 'IPv6' && isIPv6(ip)) {
try {
ip = new ipAddress.Address6(ip).correctForm();
} catch (e) {
$.error(
`Failed to parse IPv6 address: ${ip}: ${e}`,
);
}
if (/^2001::[^:]+:[^:]+:[^:]+$/.test(ip)) {
p._IP4P = ip;
const { server, port } = parseIP4P(ip);
if (server && port) {
p._domain = p.server;
p.server = server;
p.port = port;
p.resolved = true;
p._IPv4 = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
} else if (!p.resolved) {
p.resolved = false;
}
} else {
p._domain = p.server;
p.server = ip;
p.resolved = true;
p[`_${type}`] = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
}
} else {
p._domain = p.server;
p.server = ip;
p.resolved = true;
p[`_${type}`] = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
}
} else if (!p.resolved) {
p.resolved = false;
}
}
@@ -519,7 +800,7 @@ function ResolveDomainOperator({ provider, type, filter }) {
return proxies.filter((p) => {
if (filter === 'removeFailed') {
return isIP(p.server) || p['no-resolve'] || p.resolved;
return isIP(p.server) || p['_no-resolve'] || p.resolved;
} else if (filter === 'IPOnly') {
return isIP(p.server);
} else if (filter === 'IPv4Only') {
@@ -540,29 +821,55 @@ function isIP(ip) {
ResolveDomainOperator.resolver = DOMAIN_RESOLVERS;
function isAscii(str) {
// eslint-disable-next-line no-control-regex
var pattern = /^[\x00-\x7F]+$/; // ASCII 范围的 Unicode 编码
return pattern.test(str);
}
/**************************** Filters ***************************************/
// filter useless proxies
function UselessFilter() {
const KEYWORDS = [
'网址',
'流量',
'时间',
'应急',
'过期',
'Bandwidth',
'expire',
];
return {
name: 'Useless Filter',
func: RegexFilter({
regex: KEYWORDS,
keep: false,
}).func,
func: (proxies) => {
return proxies.map((proxy) => {
if (proxy.cipher && !isAscii(proxy.cipher)) {
return false;
} else if (proxy.password && !isAscii(proxy.password)) {
return false;
} else {
if (proxy.network) {
let transportHosts =
proxy[`${proxy.network}-opts`]?.headers?.Host ||
proxy[`${proxy.network}-opts`]?.headers?.host;
transportHosts = Array.isArray(transportHosts)
? transportHosts
: [transportHosts];
if (
transportHosts.some(
(host) => host && !isAscii(host),
)
) {
return false;
}
}
return !/网址|流量|时间|应急|过期|Bandwidth|expire/.test(
proxy.name,
);
}
});
},
};
}
// filter by regions
function RegionFilter(regions) {
function RegionFilter(input) {
let regions = input?.value || input;
if (!Array.isArray(regions)) {
regions = [];
}
const keep = input?.keep ?? true;
const REGION_MAP = {
HK: '🇭🇰',
TW: '🇹🇼',
@@ -570,6 +877,8 @@ function RegionFilter(regions) {
SG: '🇸🇬',
JP: '🇯🇵',
UK: '🇬🇧',
DE: '🇩🇪',
KR: '🇰🇷',
};
return {
name: 'Region Filter',
@@ -577,7 +886,8 @@ function RegionFilter(regions) {
// this would be high memory usage
return proxies.map((proxy) => {
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;
});
},
};
@@ -609,11 +919,19 @@ function buildRegex(str, ...options) {
}
// 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 {
name: 'Type Filter',
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;
});
},
};
}
@@ -631,7 +949,7 @@ function TypeFilter(types) {
1. This function name should be `filter`!
2. Always declare variables before using them!
*/
function ScriptFilter(script, targetPlatform, $arguments, source) {
function ScriptFilter(script, targetPlatform, $arguments, source, $options) {
return {
name: 'Script Filter',
func: async (proxies) => {
@@ -641,6 +959,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
'filter',
script,
$arguments,
$options,
);
output = filter(proxies, targetPlatform, { source, ...env });
})();
@@ -663,6 +982,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
return list
}`,
$arguments,
$options,
);
output = filter(proxies, targetPlatform, { source, ...env });
})();
@@ -803,36 +1123,38 @@ function clone(object) {
return JSON.parse(JSON.stringify(object));
}
// remove flag
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
.trim();
}
function createDynamicFunction(name, script, $arguments) {
function createDynamicFunction(name, script, $arguments, $options) {
const flowUtils = {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
flowTransfer,
validCheck,
getRmainingDays,
normalizeFlowHeader,
};
if ($.env.isLoon) {
return new Function(
'$arguments',
'$options',
'$substore',
'lodash',
'$persistentStore',
'$httpClient',
'$notification',
'ProxyUtils',
'yaml',
'Buffer',
'b64d',
'b64e',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
'require',
`${script}\n return ${name}`,
)(
$arguments,
$options,
$,
lodash,
// eslint-disable-next-line no-undef
@@ -842,29 +1164,45 @@ function createDynamicFunction(name, script, $arguments) {
// eslint-disable-next-line no-undef
$notification,
ProxyUtils,
ProxyUtils.yaml,
ProxyUtils.Buffer,
ProxyUtils.Base64.decode,
ProxyUtils.Base64.encode,
scriptResourceCache,
flowUtils,
produceArtifact,
eval(`typeof require !== "undefined"`) ? require : undefined,
);
} else {
return new Function(
'$arguments',
'$options',
'$substore',
'lodash',
'ProxyUtils',
'yaml',
'Buffer',
'b64d',
'b64e',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
'require',
`${script}\n return ${name}`,
)(
$arguments,
$options,
$,
lodash,
ProxyUtils,
ProxyUtils.yaml,
ProxyUtils.Buffer,
ProxyUtils.Base64.decode,
ProxyUtils.Base64.encode,
scriptResourceCache,
flowUtils,
produceArtifact,
eval(`typeof require !== "undefined"`) ? require : undefined,
);
}
}

View File

@@ -1,4 +1,5 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
import $ from '@/core/app';
export default function Clash_Producer() {
const type = 'ALL';
@@ -40,12 +41,17 @@ export default function Clash_Producer() {
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'snell' && proxy.version >= 4) ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
$.error(
`Clash 不支持前置代理字段. 已过滤节点 ${proxy.name}`,
);
return false;
}
return true;
})
@@ -81,6 +87,8 @@ export default function Clash_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
@@ -126,6 +134,18 @@ export default function Clash_Producer() {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy.network === 'ws') {
const wsPath = proxy['ws-opts']?.path;
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(wsPath);
proxy['ws-opts'].path = path;
if (ed !== '') {
proxy['ws-opts']['early-data-header-name'] =
'Sec-WebSocket-Protocol';
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
@@ -133,9 +153,14 @@ export default function Clash_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -144,17 +169,29 @@ export default function Clash_Producer() {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -1,12 +1,63 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
const ipVersions = {
dual: 'dual',
'v4-only': 'ipv4',
'v6-only': 'ipv6',
'prefer-v4': 'ipv4-prefer',
'prefer-v6': 'ipv6-prefer',
};
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
if (proxy.type === 'snell' && proxy.version >= 4) {
return false;
} else if (['juicity'].includes(proxy.type)) {
return false;
} else if (
['ss'].includes(proxy.type) &&
![
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-ccm',
'aes-192-ccm',
'aes-256-ccm',
'aes-128-gcm-siv',
'aes-256-gcm-siv',
'chacha20-ietf',
'chacha20',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'chacha8-ietf-poly1305',
'xchacha8-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305',
'lea-128-gcm',
'lea-192-gcm',
'lea-256-gcm',
'rabbit128-poly1305',
'aegis-128l',
'aegis-256',
'aez-384',
'deoxys-ii-256-128',
'rc4-md5',
'none',
].includes(proxy.cipher)
) {
// https://wiki.metacubex.one/config/proxies/ss/#cipher
return false;
}
return true;
@@ -30,9 +81,10 @@ export default function ClashMeta_Producer() {
isPresent(proxy, 'cipher') &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
@@ -84,6 +136,8 @@ export default function ClashMeta_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
@@ -100,6 +154,9 @@ export default function ClashMeta_Producer() {
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'];
}
}
@@ -141,6 +198,18 @@ export default function ClashMeta_Producer() {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy.network === 'ws') {
const wsPath = proxy['ws-opts']?.path;
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(wsPath);
proxy['ws-opts'].path = path;
if (ed !== '') {
proxy['ws-opts']['early-data-header-name'] =
'Sec-WebSocket-Protocol';
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
@@ -149,9 +218,14 @@ export default function ClashMeta_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -160,16 +234,38 @@ export default function ClashMeta_Producer() {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal' || opts['delete-underscore-fields']) {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
if (proxy['ip-version']) {
proxy['ip-version'] =
ipVersions[proxy['ip-version']] || proxy['ip-version'];
}
return proxy;
});

View File

@@ -0,0 +1,420 @@
import { isPresent } from './utils';
export default function Egern_Producer() {
const type = 'ALL';
const produce = (proxies, type) => {
// https://egernapp.com/zh-CN/docs/configuration/proxies
const list = proxies
.filter((proxy) => {
// if (opts['include-unsupported-proxy']) return true;
if (
![
'http',
'socks5',
'ss',
'trojan',
'hysteria2',
'vless',
'vmess',
'tuic',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
((proxy.plugin === 'obfs' &&
!['http', 'tls'].includes(
proxy['plugin-opts']?.mode,
)) ||
![
'chacha20-ietf-poly1305',
'chacha20-poly1305',
'aes-256-gcm',
'aes-128-gcm',
'none',
'tbale',
'rc4',
'rc4-md5',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'bf-cfb',
'camellia-128-cfb',
'camellia-192-cfb',
'camellia-256-cfb',
'cast5-cfb',
'des-cfb',
'idea-cfb',
'rc2-cfb',
'seed-cfb',
'salsa20',
'chacha20',
'chacha20-ietf',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher))) ||
(proxy.type === 'vmess' &&
!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) ||
(proxy.type === 'trojan' &&
!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network) ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts'] ||
(!['http', 'ws', 'tcp'].includes(proxy.network) &&
proxy.network))) ||
(proxy.type === 'tuic' &&
proxy.token &&
proxy.token.length !== 0)
) {
return false;
}
return true;
})
.map((proxy) => {
const original = { ...proxy };
if (proxy.tls && !proxy.sni) {
proxy.sni = proxy.server;
}
const prev_hop =
proxy.prev_hop ||
proxy['underlying-proxy'] ||
proxy['dialer-proxy'] ||
proxy.detour;
if (proxy.type === 'http') {
proxy = {
type: 'http',
name: proxy.name,
server: proxy.server,
port: proxy.port,
username: proxy.username,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
next_hop: proxy.next_hop,
};
} else if (proxy.type === 'socks5') {
proxy = {
type: 'socks5',
name: proxy.name,
server: proxy.server,
port: proxy.port,
username: proxy.username,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
};
} else if (proxy.type === 'ss') {
proxy = {
type: 'shadowsocks',
name: proxy.name,
method:
proxy.cipher === 'chacha20-ietf-poly1305'
? 'chacha20-poly1305'
: proxy.cipher,
server: proxy.server,
port: proxy.port,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
};
if (original.plugin === 'obfs') {
proxy.obfs = original['plugin-opts'].mode;
proxy.obfs_host = original['plugin-opts'].host;
proxy.obfs_uri = original['plugin-opts'].path;
}
} else if (proxy.type === 'hysteria2') {
proxy = {
type: 'hysteria2',
name: proxy.name,
server: proxy.server,
port: proxy.port,
auth: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
};
if (
original['obfs-password'] &&
original.obfs == 'salamander'
) {
proxy.obfs = 'salamander';
proxy.obfs_password = original['obfs-password'];
}
} else if (proxy.type === 'tuic') {
proxy = {
type: 'tuic',
name: proxy.name,
server: proxy.server,
port: proxy.port,
uuid: proxy.uuid,
password: proxy.password,
next_hop: proxy.next_hop,
sni: proxy.sni,
alpn: Array.isArray(proxy.alpn)
? proxy.alpn
: [proxy.alpn || 'h3'],
skip_tls_verify: proxy['skip-cert-verify'],
port_hopping: proxy.ports,
port_hopping_interval: proxy['hop-interval'],
};
} else if (proxy.type === 'trojan') {
if (proxy.network === 'ws') {
proxy.websocket = {
path: proxy['ws-opts']?.path,
host: proxy['ws-opts']?.headers?.Host,
};
}
proxy = {
type: 'trojan',
name: proxy.name,
server: proxy.server,
port: proxy.port,
password: proxy.password,
tfo: proxy.tfo || proxy['fast-open'],
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
sni: proxy.sni,
skip_tls_verify: proxy['skip-cert-verify'],
websocket: proxy.websocket,
};
} else if (proxy.type === 'vmess') {
// Egern传输层支持 ws/wss/http1/http2/tls不配置则为 tcp
let security = proxy.cipher;
if (
security &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
].includes(security)
) {
security = 'auto';
}
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
path: proxy['ws-opts']?.path,
headers: {
Host: proxy['ws-opts']?.headers?.Host,
},
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
} else if (proxy.network === 'http') {
proxy.transport = {
http1: {
method: proxy['http-opts']?.method,
path: Array.isArray(proxy['http-opts']?.path)
? proxy['http-opts']?.path[0]
: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
)
? proxy['http-opts']?.headers?.Host[0]
: proxy['http-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'h2') {
proxy.transport = {
http2: {
method: proxy['h2-opts']?.method,
path: Array.isArray(proxy['h2-opts']?.path)
? proxy['h2-opts']?.path[0]
: proxy['h2-opts']?.path,
headers: {
Host: Array.isArray(
proxy['h2-opts']?.headers?.Host,
)
? proxy['h2-opts']?.headers?.Host[0]
: proxy['h2-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (
(proxy.network === 'tcp' || !proxy.network) &&
proxy.tls
) {
proxy.transport = {
tls: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
}
proxy = {
type: 'vmess',
name: proxy.name,
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
transport: proxy.transport,
// sni: proxy.sni,
// skip_tls_verify: proxy['skip-cert-verify'],
};
} else if (proxy.type === 'vless') {
if (proxy.network === 'ws') {
proxy.transport = {
[proxy.tls ? 'wss' : 'ws']: {
path: proxy['ws-opts']?.path,
headers: {
Host: proxy['ws-opts']?.headers?.Host,
},
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
} else if (proxy.network === 'http') {
proxy.transport = {
http: {
method: proxy['http-opts']?.method,
path: Array.isArray(proxy['http-opts']?.path)
? proxy['http-opts']?.path[0]
: proxy['http-opts']?.path,
headers: {
Host: Array.isArray(
proxy['http-opts']?.headers?.Host,
)
? proxy['http-opts']?.headers?.Host[0]
: proxy['http-opts']?.headers?.Host,
},
skip_tls_verify: proxy['skip-cert-verify'],
},
};
} else if (proxy.network === 'tcp' || !proxy.network) {
proxy.transport = {
[proxy.tls ? 'tls' : 'tcp']: {
sni: proxy.tls ? proxy.sni : undefined,
skip_tls_verify: proxy.tls
? proxy['skip-cert-verify']
: undefined,
},
};
}
proxy = {
type: 'vless',
name: proxy.name,
server: proxy.server,
port: proxy.port,
user_id: proxy.uuid,
security: proxy.cipher,
tfo: proxy.tfo || proxy['fast-open'],
legacy: proxy.legacy,
udp_relay:
proxy.udp || proxy.udp_relay || proxy.udp_relay,
next_hop: proxy.next_hop,
transport: proxy.transport,
// sni: proxy.sni,
// skip_tls_verify: proxy['skip-cert-verify'],
};
}
if (
[
'http',
'socks5',
'ss',
'trojan',
'vless',
'vmess',
].includes(original.type)
) {
if (isPresent(original, 'shadow-tls-password')) {
if (original['shadow-tls-version'] != 3)
throw new Error(
`shadow-tls version ${original['shadow-tls-version']} is not supported`,
);
proxy.shadow_tls = {
password: original['shadow-tls-password'],
sni: original['shadow-tls-sni'],
};
} else if (
['shadow-tls'].includes(original.plugin) &&
original['plugin-opts']
) {
if (original['plugin-opts'].version != 3)
throw new Error(
`shadow-tls version ${original['plugin-opts'].version} is not supported`,
);
proxy.shadow_tls = {
password: original['plugin-opts'].password,
sni: original['plugin-opts'].host,
};
}
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (proxy.transport) {
for (const key in proxy.transport) {
if (
Object.keys(proxy.transport[key]).length === 0 ||
Object.values(proxy.transport[key]).every(
(v) => v == null,
)
) {
delete proxy.transport[key];
}
}
if (Object.keys(proxy.transport).length === 0) {
delete proxy.transport;
}
}
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
return {
[proxy.type]: {
...proxy,
type: undefined,
prev_hop,
},
};
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -10,27 +10,47 @@ import QX_Producer from './qx';
import Shadowrocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
import Egern_Producer from './egern';
function JSON_Producer() {
const type = 'ALL';
const produce = (proxies) => JSON.stringify(proxies, null, 2);
const produce = (proxies, type) =>
type === 'internal' ? proxies : JSON.stringify(proxies, null, 2);
return { type, produce };
}
export default {
qx: QX_Producer(),
QX: QX_Producer(),
QuantumultX: QX_Producer(),
surge: Surge_Producer(),
Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(),
Clash: Clash_Producer(),
meta: ClashMeta_Producer(),
clashmeta: ClashMeta_Producer(),
'clash.meta': ClashMeta_Producer(),
'Clash.Meta': ClashMeta_Producer(),
ClashMeta: ClashMeta_Producer(),
mihomo: ClashMeta_Producer(),
Mihomo: ClashMeta_Producer(),
uri: URI_Producer(),
URI: URI_Producer(),
v2: V2Ray_Producer(),
v2ray: V2Ray_Producer(),
V2Ray: V2Ray_Producer(),
json: JSON_Producer(),
JSON: JSON_Producer(),
stash: Stash_Producer(),
Stash: Stash_Producer(),
shadowrocket: Shadowrocket_Producer(),
Shadowrocket: Shadowrocket_Producer(),
ShadowRocket: Shadowrocket_Producer(),
surfboard: Surfboard_Producer(),
Surfboard: Surfboard_Producer(),
singbox: singbox_Producer(),
'sing-box': singbox_Producer(),
egern: Egern_Producer(),
Egern: Egern_Producer(),
};

View File

@@ -3,8 +3,16 @@ const targetPlatform = 'Loon';
import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils';
const ipVersions = {
dual: 'dual',
ipv4: 'v4-only',
ipv6: 'v6-only',
'ipv4-prefer': 'prefer-v4',
'ipv6-prefer': 'prefer-v6',
};
export default function Loon_Producer() {
const produce = (proxy) => {
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
@@ -13,11 +21,13 @@ export default function Loon_Producer() {
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy);
return vmess(proxy, opts['include-unsupported-proxy']);
case 'vless':
return vless(proxy);
return vless(proxy, opts['include-unsupported-proxy']);
case 'http':
return http(proxy);
case 'socks5':
return socks5(proxy);
case 'wireguard':
return wireguard(proxy);
case 'hysteria2':
@@ -54,6 +64,8 @@ function shadowsocks(proxy) {
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
@@ -74,19 +86,68 @@ function shadowsocks(proxy) {
`,obfs-uri=${proxy['plugin-opts'].path}`,
'plugin-opts.path',
);
} else {
} else if (!['shadow-tls'].includes(proxy.plugin)) {
throw new Error(`plugin ${proxy.plugin} is not supported`);
}
}
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -107,14 +168,63 @@ function shadowsocksr(proxy) {
result.appendIfPresent(`,obfs=${proxy.obfs}`, 'obfs');
result.appendIfPresent(`,obfs-param=${proxy['obfs-param']}`, 'obfs-param');
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
const version = proxy['plugin-opts'].version;
if (password) {
result.append(`,shadow-tls-password=${password}`);
if (host) {
result.append(`,shadow-tls-sni=${host}`);
}
if (version) {
if (version < 2) {
throw new Error(
`shadow-tls version ${version} is not supported`,
);
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -151,19 +261,38 @@ function trojan(proxy) {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
function vmess(proxy) {
const isReality = !!proxy['reality-opts'];
const result = new Result(proxy);
result.append(
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
@@ -211,12 +340,32 @@ function vmess(proxy) {
'skip-cert-verify',
);
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
if (isReality) {
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,public-key="${proxy['reality-opts']['public-key']}"`,
'reality-opts.public-key',
);
result.appendIfPresent(
`,short-id=${proxy['reality-opts']['short-id']}`,
'reality-opts.short-id',
);
} else {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
}
// AEAD
if (isPresent(proxy, 'aead')) {
result.append(`,alterId=0`);
result.append(`,alterId=${proxy.aead ? 0 : 1}`);
} else {
result.append(`,alterId=${proxy.alterId}`);
}
@@ -224,17 +373,34 @@ function vmess(proxy) {
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
function vless(proxy) {
if (proxy['reality-opts']) {
throw new Error(`VLESS REALITY is unsupported`);
let isXtls = false;
const isReality = !!proxy['reality-opts'];
if (typeof proxy.flow !== 'undefined') {
if (['xtls-rprx-vision'].includes(proxy.flow)) {
isXtls = true;
} else {
throw new Error(`VLESS flow(${proxy.flow}) is not supported`);
}
}
const result = new Result(proxy);
result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
@@ -282,16 +448,48 @@ function vless(proxy) {
'skip-cert-verify',
);
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
if (isXtls) {
result.appendIfPresent(`,flow=${proxy.flow}`, 'flow');
}
if (isReality) {
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,public-key="${proxy['reality-opts']['public-key']}"`,
'reality-opts.public-key',
);
result.appendIfPresent(
`,short-id=${proxy['reality-opts']['short-id']}`,
'reality-opts.short-id',
);
} else {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -314,6 +512,53 @@ function http(proxy) {
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
function socks5(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=socks5,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// tls
result.appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
// sni
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
// tls verification
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
return result.toString();
}
@@ -378,13 +623,22 @@ function wireguard(proxy) {
presharedKey ?? ''
}}]`,
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-mode=${ip_version}`, 'ip-version');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
if (proxy['obfs-password'] && proxy.obfs != 'salamander') {
throw new Error(`only salamander obfs is supported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
@@ -393,14 +647,33 @@ function hysteria2(proxy) {
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
result.appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
if (proxy['obfs-password'] && proxy.obfs == 'salamander') {
result.append(`,salamander-password=${proxy['obfs-password']}`);
}
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// block-quic
if (proxy['block-quic'] === 'on') {
result.append(',block-quic=true');
} else if (proxy['block-quic'] === 'off') {
result.append(',block-quic=false');
}
// udp
if (proxy.udp) {
result.append(`,udp=true`);
@@ -413,6 +686,8 @@ function hysteria2(proxy) {
);
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();
}

View File

@@ -3,6 +3,7 @@ import { isPresent, Result } from './utils';
const targetPlatform = 'QX';
export default function QX_Producer() {
// eslint-disable-next-line no-unused-vars
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
@@ -18,13 +19,7 @@ export default function QX_Producer() {
case 'socks5':
return socks5(proxy);
case 'vless':
if (opts['include-unsupported-proxy']) {
return vless(proxy);
} else {
throw new Error(
`Platform ${targetPlatform}(App Store Release) does not support proxy type: ${proxy.type}`,
);
}
return vless(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -63,6 +58,8 @@ function shadowsocks(proxy) {
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
@@ -133,6 +130,20 @@ function shadowsocks(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// udp over tcp
if (proxy['_ssr_python_uot']) {
append(`,udp-over-tcp=true`);
} else if (proxy['udp-over-tcp']) {
if (
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
) {
append(`,udp-over-tcp=sp.v1`);
} else if (proxy['udp-over-tcp-version'] === 2) {
append(`,udp-over-tcp=sp.v2`);
}
}
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
@@ -394,6 +405,8 @@ function vless(proxy) {
else append(`,obfs=ws`);
} else if (proxy.network === 'http') {
append(`,obfs=http`);
} else if (['tcp'].includes(proxy.network)) {
if (proxy.tls) append(`,obfs=over-tls`);
} else if (!['tcp'].includes(proxy.network)) {
throw new Error(`network ${proxy.network} is unsupported`);
}

View File

@@ -1,12 +1,15 @@
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 produce = (proxies, type, opts = {}) => {
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (proxy.type === 'snell' && String(proxy.version) === '4') {
if (proxy.type === 'snell' && proxy.version >= 4) {
return false;
} else if (['mieru'].includes(proxy.type)) {
return false;
}
return true;
@@ -30,9 +33,10 @@ export default function ShadowRocket_Producer() {
isPresent(proxy, 'cipher') &&
![
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
@@ -100,11 +104,28 @@ export default function ShadowRocket_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
} else if (proxy.type === 'ss') {
if (
isPresent(proxy, 'shadow-tls-password') &&
!isPresent(proxy, 'plugin')
) {
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: proxy['shadow-tls-sni'],
password: proxy['shadow-tls-password'],
version: proxy['shadow-tls-version'],
};
delete proxy['shadow-tls-password'];
delete proxy['shadow-tls-sni'];
delete proxy['shadow-tls-version'];
}
}
if (
@@ -145,6 +166,18 @@ export default function ShadowRocket_Producer() {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy.network === 'ws') {
const wsPath = proxy['ws-opts']?.path;
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(wsPath);
proxy['ws-opts'].path = path;
if (ed !== '') {
proxy['ws-opts']['early-data-header-name'] =
'Sec-WebSocket-Protocol';
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
@@ -152,9 +185,14 @@ export default function ShadowRocket_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;
}
@@ -163,16 +201,33 @@ export default function ShadowRocket_Producer() {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -1,6 +1,34 @@
import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
import { isIPv4, isIPv6 } from '@/utils';
const ipVersions = {
ipv4: 'ipv4_only',
ipv6: 'ipv6_only',
'v4-only': 'ipv4_only',
'v6-only': 'ipv6_only',
'ipv4-prefer': 'prefer_ipv4',
'ipv6-prefer': 'prefer_ipv6',
'prefer-v4': 'prefer_ipv4',
'prefer-v6': 'prefer_ipv6',
};
const ipVersionParser = (proxy, parsedProxy) => {
const strategy = ipVersions[proxy['ip-version']];
if (proxy._dns_server && strategy) {
parsedProxy.domain_resolver = {
server: proxy._dns_server,
strategy,
};
}
};
const detourParser = (proxy, parsedProxy) => {
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
};
const networkParser = (proxy, parsedProxy) => {
if (['tcp', 'udp'].includes(proxy._network))
parsedProxy.network = proxy._network;
};
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
@@ -23,12 +51,34 @@ const smuxParser = (smux, proxy) => {
if (smux['min-streams'])
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
if (smux.padding) proxy.multiplex.padding = true;
if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) {
proxy.multiplex.brutal = {
enabled: true,
};
if (smux['brutal-opts']?.up)
proxy.multiplex.brutal.up_mbps = parseInt(
`${smux['brutal-opts']?.up}`,
10,
);
if (smux['brutal-opts']?.down)
proxy.multiplex.brutal.down_mbps = parseInt(
`${smux['brutal-opts']?.down}`,
10,
);
}
};
const wsParser = (proxy, parsedProxy) => {
const transport = { type: 'ws', headers: {} };
if (proxy['ws-opts']) {
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
const {
path: wsPath = '',
headers: wsHeaders = {},
'max-early-data': max_early_data,
'early-data-header-name': early_data_header_name,
} = proxy['ws-opts'];
transport.early_data_header_name = early_data_header_name;
transport.max_early_data = parseInt(max_early_data, 10);
if (wsPath !== '') transport.path = `${wsPath}`;
if (Object.keys(wsHeaders).length > 0) {
const headers = {};
@@ -198,13 +248,8 @@ const tlsParser = (proxy, parsedProxy) => {
parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_sStr;
if (proxy['ca-str']) parsedProxy.tls.certificate = proxy['ca-str'];
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
if (proxy['reality-opts']) {
parsedProxy.tls.reality = { enabled: true };
if (proxy['reality-opts']['public-key'])
@@ -213,7 +258,17 @@ const tlsParser = (proxy, parsedProxy) => {
if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id'];
parsedProxy.tls.utls = { enabled: true };
}
if (
!['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&
proxy['client-fingerprint'] &&
proxy['client-fingerprint'] !== ''
)
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
};
@@ -228,6 +283,13 @@ const sshParser = (proxy = {}) => {
throw 'invalid port';
if (proxy.username) parsedProxy.user = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
// https://wiki.metacubex.one/config/proxies/ssh
// https://sing-box.sagernet.org/zh/configuration/outbound/ssh
if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
if (proxy['private-key'])
parsedProxy.private_key_path = proxy['private-key'];
if (proxy['private-key-passphrase'])
parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
if (proxy['server-fingerprint']) {
parsedProxy.host_key = [proxy['server-fingerprint']];
// https://manual.nssurge.com/policy/ssh.html
@@ -237,8 +299,13 @@ const sshParser = (proxy = {}) => {
proxy['server-fingerprint'].split(' ')[0],
];
}
if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];
if (proxy['host-key-algorithms'])
parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -264,7 +331,9 @@ const httpParser = (proxy = {}) => {
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -284,7 +353,10 @@ const socks5Parser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -296,6 +368,17 @@ const shadowTLSParser = (proxy = {}) => {
password: proxy.password,
detour: `${proxy.name}_shadowtls`,
};
if (proxy.uot) ssPart.udp_over_tcp = true;
if (proxy['udp-over-tcp']) {
ssPart.udp_over_tcp = {
enabled: true,
version:
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
? 1
: 2,
};
}
const stPart = {
tag: `${proxy.name}_shadowtls`,
type: 'shadowtls',
@@ -316,7 +399,9 @@ const shadowTLSParser = (proxy = {}) => {
throw '端口值非法';
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
tfoParser(proxy, stPart);
detourParser(proxy, stPart);
smuxParser(proxy.smux, ssPart);
ipVersionParser(proxy, stPart);
return { type: 'ss-with-st', ssPart, stPart };
};
const ssParser = (proxy = {}) => {
@@ -331,10 +416,22 @@ const ssParser = (proxy = {}) => {
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) {
parsedProxy.udp_over_tcp = {
enabled: true,
version:
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
? 1
: 2,
};
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
if (proxy.plugin) {
const optArr = [];
if (proxy.plugin === 'obfs') {
@@ -411,7 +508,9 @@ const ssrParser = (proxy = {}) => {
parsedProxy.protocol_param = proxy['protocol-param'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -445,10 +544,12 @@ const vmessParser = (proxy = {}) => {
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -463,14 +564,18 @@ const vlessParser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
// if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow;
if (proxy.flow != null) parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const trojanParser = (proxy = {}) => {
@@ -487,10 +592,12 @@ const trojanParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const hysteriaParser = (proxy = {}) => {
@@ -509,12 +616,13 @@ const hysteriaParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// eslint-disable-next-line no-control-regex
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
if (reg.test(`${proxy.up}`)) {
// sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps
if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) {
parsedProxy.up = `${proxy.up}`;
} else {
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
}
if (reg.test(`${proxy.down}`)) {
if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) {
parsedProxy.down = `${proxy.down}`;
} else {
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
@@ -534,9 +642,12 @@ const hysteriaParser = (proxy = {}) => {
parsedProxy.disable_mtu_discovery = true;
}
}
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
@@ -551,15 +662,26 @@ const hysteria2Parser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['hop-interval'])
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
? `${proxy['hop-interval']}s`
: proxy['hop-interval'];
if (proxy['ports'])
parsedProxy.server_ports = proxy['ports']
.split(/\s*,\s*/)
.map((p) => p.replace(/\s*-\s*/g, ':'));
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const tuic5Parser = (proxy = {}) => {
@@ -586,17 +708,46 @@ const tuic5Parser = (proxy = {}) => {
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const anytlsParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'anytls',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (/^\d+$/.test(proxy['idle-session-check-interval']))
parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
if (/^\d+$/.test(proxy['idle-session-timeout']))
parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
if (/^\d+$/.test(proxy['min-idle-session']))
parsedProxy.min_idle_session = parseInt(
`${proxy['min-idle-session']}`,
10,
);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6']
.map((i) => proxy[i])
.filter((i) => i)
.map((i) => (/\\/.test(i) ? i : `${i}/32`));
.map((i) => {
if (isIPv4(i)) return `${i}/32`;
if (isIPv6(i)) return `${i}/128`;
})
.filter((i) => i);
const parsedProxy = {
tag: proxy.name,
type: 'wireguard',
@@ -612,7 +763,7 @@ const wireguardParser = (proxy = {}) => {
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') {
parsedProxy.reserved.push(proxy.reserved);
parsedProxy.reserved = proxy.reserved;
} else if (Array.isArray(proxy.reserved)) {
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
} else {
@@ -639,8 +790,11 @@ const wireguardParser = (proxy = {}) => {
parsedProxy.peers.push(peer);
}
}
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -755,7 +909,12 @@ export default function singbox_Producer() {
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(hysteria2Parser(proxy));
list.push(
hysteria2Parser(
proxy,
opts['include-unsupported-proxy'],
),
);
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {
@@ -769,6 +928,9 @@ export default function singbox_Producer() {
case 'wireguard':
list.push(wireguardParser(proxy));
break;
case 'anytls':
list.push(anytlsParser(proxy));
break;
default:
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
@@ -779,7 +941,10 @@ export default function singbox_Producer() {
$.error(e.message ?? e);
}
});
return type === 'internal' ? list : JSON.stringify(list, null, 2);
return type === 'internal'
? list
: JSON.stringify({ outbounds: list }, null, 2);
};
return { type, produce };
}

View File

@@ -1,4 +1,5 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
import $ from '@/core/app';
export default function Stash_Producer() {
const type = 'ALL';
@@ -6,7 +7,6 @@ export default function Stash_Producer() {
// https://stash.wiki/proxy-protocols/proxy-types#shadowsocks
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'ss',
@@ -21,6 +21,8 @@ export default function Stash_Producer() {
'wireguard',
'hysteria',
'hysteria2',
'ssh',
'juicity',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
@@ -38,11 +40,20 @@ export default function Stash_Producer() {
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' && proxy['reality-opts'])
(proxy.type === 'snell' && proxy.version >= 4) ||
(proxy.type === 'vless' &&
proxy['reality-opts'] &&
!['xtls-rprx-vision'].includes(proxy.flow))
) {
return false;
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
$.error(
`Stash 暂不支持前置代理字段. 已过滤节点 ${proxy.name}. 请使用 代理的转发链 https://stash.wiki/proxy-protocols/proxy-groups#relay`,
);
return false;
}
return true;
})
@@ -180,6 +191,8 @@ export default function Stash_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'snell' && proxy.version < 3) {
delete proxy.udp;
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
@@ -225,6 +238,18 @@ export default function Stash_Producer() {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy.network === 'ws') {
const wsPath = proxy['ws-opts']?.path;
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(wsPath);
proxy['ws-opts'].path = path;
if (ed !== '') {
proxy['ws-opts']['early-data-header-name'] =
'Sec-WebSocket-Protocol';
proxy['ws-opts']['max-early-data'] = parseInt(ed, 10);
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
@@ -232,16 +257,22 @@ export default function Stash_Producer() {
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
[
'trojan',
'tuic',
'hysteria',
'hysteria2',
'juicity',
'anytls',
].includes(proxy.type)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
proxy['server-cert-fingerprint'] = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
@@ -257,11 +288,22 @@ export default function Stash_Producer() {
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
}
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
return proxy;
});

View File

@@ -6,7 +6,7 @@ const targetPlatform = 'Surfboard';
export default function Surfboard_Producer() {
const produce = (proxy) => {
proxy.name = proxy.name.replace(/=/g, '');
proxy.name = proxy.name.replace(/=|,/g, '');
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);

View File

@@ -14,15 +14,21 @@ const ipVersions = {
export default function Surge_Producer() {
const produce = (proxy, type, opts = {}) => {
proxy.name = proxy.name.replace(/=|,/g, '');
if (proxy.ports) {
proxy.ports = String(proxy.ports);
}
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
return shadowsocks(proxy, opts['include-unsupported-proxy']);
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy);
return vmess(proxy, opts['include-unsupported-proxy']);
case 'http':
return http(proxy);
case 'direct':
return direct(proxy);
case 'socks5':
return socks5(proxy);
case 'snell':
@@ -81,12 +87,14 @@ function shadowsocks(proxy) {
'chacha20',
'chacha20-ietf',
'none',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -132,7 +140,10 @@ function shadowsocks(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -146,6 +157,8 @@ function shadowsocks(proxy) {
`,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;
@@ -163,6 +176,11 @@ function shadowsocks(proxy) {
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
@@ -181,7 +199,7 @@ function shadowsocks(proxy) {
function trojan(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -229,7 +247,10 @@ function trojan(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -257,7 +278,7 @@ function trojan(proxy) {
return result.toString();
}
function vmess(proxy) {
function vmess(proxy, includeUnsupportedProxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
@@ -271,7 +292,7 @@ function vmess(proxy) {
);
// transport
handleTransport(result, proxy);
handleTransport(result, proxy, includeUnsupportedProxy);
// AEAD
if (isPresent(proxy, 'aead')) {
@@ -315,7 +336,10 @@ function vmess(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -346,9 +370,16 @@ function vmess(proxy) {
function ssh(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
result.appendIfPresent(
`,private-key=${proxy['keystore-private-key']}`,
'keystore-private-key',
);
result.appendIfPresent(
`,idle-timeout=${proxy['idle-timeout']}`,
'idle-timeout',
@@ -385,7 +416,10 @@ function ssh(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
@@ -399,11 +433,14 @@ function ssh(proxy) {
return result.toString();
}
function http(proxy) {
if (proxy.headers && Object.keys(proxy.headers).length > 0) {
throw new Error(`headers is unsupported`);
}
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -445,7 +482,10 @@ function http(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -472,13 +512,61 @@ function http(proxy) {
return result.toString();
}
function direct(proxy) {
const result = new Result(proxy);
const type = 'direct';
result.append(`${proxy.name}=${type}`);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function socks5(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -522,7 +610,10 @@ function socks5(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -597,7 +688,10 @@ function snell(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -638,7 +732,7 @@ function tuic(proxy) {
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
@@ -646,6 +740,15 @@ function tuic(proxy) {
'alpn',
);
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -687,7 +790,10 @@ function tuic(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -731,8 +837,8 @@ function wireguard(proxy) {
}
const result = new Result(proxy);
result.append(`# WireGuard Proxy ${proxy.name}
${proxy.name}=wireguard`);
result.append(`# > WireGuard Proxy ${proxy.name}
# ${proxy.name}=wireguard`);
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
@@ -761,7 +867,10 @@ ${proxy.name}=wireguard`);
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -787,7 +896,7 @@ ${proxy.name}=wireguard`);
);
result.append(`
# WireGuard Section ${proxy.name}
# > WireGuard Section ${proxy.name}
[WireGuard ${proxy['section-name']}]
private-key = ${proxy['private-key']}`);
@@ -816,7 +925,7 @@ private-key = ${proxy['private-key']}`);
}
const peer = {
'public-key': proxy['public-key'],
'allowed-ips': allowedIps,
'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
endpoint: `${proxy.server}:${proxy.port}`,
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
'client-id': reserved,
@@ -860,7 +969,10 @@ function wireguard_surge(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -895,7 +1007,16 @@ function hysteria2(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -936,7 +1057,10 @@ function hysteria2(proxy) {
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(`,interface=${proxy['interface']}`, 'interface');
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
@@ -972,7 +1096,7 @@ function hysteria2(proxy) {
return result.toString();
}
function handleTransport(result, proxy) {
function handleTransport(result, proxy, includeUnsupportedProxy) {
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,ws=true`);
@@ -998,7 +1122,13 @@ function handleTransport(result, proxy) {
}
}
} else {
throw new Error(`network ${proxy.network} is unsupported`);
if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
$.info(
`Include Unsupported Proxy: nework ${proxy.network} -> tcp`,
);
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
}
}
}

View File

@@ -1,6 +1,8 @@
import { Result } from './utils';
import { Base64 } from 'js-base64';
import { Result, isPresent } from './utils';
import Surge_Producer from './surge';
import { isIPv4, isIPv6, isPresent } from '@/utils';
import ClashMeta_Producer from './clashmeta';
import { isIPv4, isIPv6 } from '@/utils';
import $ from '@/core/app';
const targetPlatform = 'SurgeMac';
@@ -8,14 +10,28 @@ const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() {
const produce = (proxy) => {
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'external':
return external(proxy);
case 'ssr':
return shadowsocksr(proxy);
default:
return surge_Producer.produce(proxy);
// case 'ssr':
// return shadowsocksr(proxy);
default: {
try {
return surge_Producer.produce(proxy, type, opts);
} catch (e) {
if (opts.useMihomoExternal) {
$.log(
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
);
return mihomo(proxy, type, opts);
} else {
throw new Error(
`Surge for macOS 可手动指定链接参数 target=SurgeMac 或在 同步配置 中指定 SurgeMac 来启用 mihomo 支援 Surge 本身不支持的协议`,
);
}
}
}
}
};
return { produce };
@@ -60,6 +76,7 @@ function external(proxy) {
return result.toString();
}
// eslint-disable-next-line no-unused-vars
function shadowsocksr(proxy) {
const external_proxy = {
...proxy,
@@ -84,6 +101,7 @@ function shadowsocksr(proxy) {
for (const [key, value] of Object.entries({
cipher: '-m',
obfs: '-o',
'obfs-param': '-g',
password: '-k',
port: '-p',
protocol: '-O',
@@ -92,12 +110,82 @@ function shadowsocksr(proxy) {
'local-port': '-l',
'local-address': '-b',
})) {
external_proxy.args.push(value);
external_proxy.args.push(external_proxy[key]);
if (external_proxy[key] != null) {
external_proxy.args.push(value);
external_proxy.args.push(external_proxy[key]);
}
}
return external(external_proxy);
}
// eslint-disable-next-line no-unused-vars
function mihomo(proxy, type, opts) {
const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];
if (clashProxy) {
const localPort = opts?.localPort || proxy._localPort || 65535;
const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])
? false
: true;
const external_proxy = {
name: proxy.name,
type: 'external',
exec: proxy._exec || '/usr/local/bin/mihomo',
'local-port': localPort,
args: [
'-config',
Base64.encode(
JSON.stringify({
'mixed-port': localPort,
ipv6,
mode: 'global',
dns: {
enable: true,
ipv6,
'default-nameserver': opts?.defaultNameserver ||
proxy._defaultNameserver || [
'180.76.76.76',
'52.80.52.52',
'119.28.28.28',
'223.6.6.6',
],
nameserver: opts?.nameserver ||
proxy._nameserver || [
'https://doh.pub/dns-query',
'https://dns.alidns.com/dns-query',
'https://doh-pure.onedns.net/dns-query',
],
},
proxies: [
{
...clashProxy,
name: 'proxy',
},
],
'proxy-groups': [
{
name: 'GLOBAL',
type: 'select',
proxies: ['proxy'],
},
],
}),
),
],
addresses: [],
};
// https://manual.nssurge.com/policy/external-proxy.html
if (isIP(proxy.server)) {
external_proxy.addresses.push(proxy.server);
} else {
$.log(
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
);
}
opts.localPort = localPort - 1;
return external(external_proxy);
}
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);

View File

@@ -8,18 +8,45 @@ export default function URI_Producer() {
let result = '';
delete proxy.subName;
delete proxy.collectionName;
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null) {
delete proxy[key];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy.server && isIPv6(proxy.server)) {
if (
!['vmess'].includes(proxy.type) &&
proxy.server &&
isIPv6(proxy.server)
) {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) {
case 'socks5':
result = `socks://${encodeURIComponent(
Base64.encode(
`${proxy.username ?? ''}:${proxy.password ?? ''}`,
),
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
break;
case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
proxy.port
}/`;
result = `ss://${
proxy.cipher?.startsWith('2022-blake3-')
? `${encodeURIComponent(
proxy.cipher,
)}:${encodeURIComponent(proxy.password)}`
: Base64.encode(userinfo)
}@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;
if (proxy.plugin) {
result += '?plugin=';
const opts = proxy['plugin-opts'];
@@ -38,6 +65,11 @@ export default function URI_Producer() {
}${opts.tls ? ';tls' : ''}`,
);
break;
case 'shadow-tls':
result += encodeURIComponent(
`shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
);
break;
default:
throw new Error(
`Unsupported plugin option: ${proxy.plugin}`,
@@ -48,7 +80,9 @@ export default function URI_Producer() {
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
}
if (proxy.tfo) {
result = `${result}${proxy.plugin ? '&' : '?'}tfo=1`;
result = `${result}${
proxy.plugin || proxy['udp-over-tcp'] ? '&' : '?'
}tfo=1`;
}
result += `#${encodeURIComponent(proxy.name)}`;
break;
@@ -75,17 +109,27 @@ export default function URI_Producer() {
if (proxy.network === 'http') {
net = 'tcp';
type = 'http';
} else if (
proxy.network === 'ws' &&
proxy['ws-opts']?.['v2ray-http-upgrade']
) {
net = 'httpupgrade';
}
result = {
v: '2',
ps: proxy.name,
add: proxy.server,
port: proxy.port,
port: `${proxy.port}`,
id: proxy.uuid,
type,
aid: 0,
aid: `${proxy.alterId || 0}`,
scy: proxy.cipher,
net,
type,
tls: proxy.tls ? 'tls' : '',
alpn: Array.isArray(proxy.alpn)
? proxy.alpn.join(',')
: proxy.alpn,
fp: proxy['client-fingerprint'],
};
if (proxy.tls && proxy.sni) {
result.sni = proxy.sni;
@@ -96,16 +140,7 @@ export default function URI_Producer() {
proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
@@ -115,6 +150,33 @@ export default function URI_Producer() {
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
result.host =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
} else if (['kcp', 'quic'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-type`
] || 'none';
result.host =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-host`
];
result.path =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-path`
];
} else {
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
}
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));
@@ -124,6 +186,7 @@ export default function URI_Producer() {
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
let spx = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
@@ -134,6 +197,10 @@ export default function URI_Producer() {
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
spx = `&spx=${encodeURIComponent(spiderX)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
@@ -163,14 +230,35 @@ export default function URI_Producer() {
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let vlessTransport = `&type=${encodeURIComponent(
proxy.network,
)}`;
let extra = '';
if (proxy._extra) {
extra = `&extra=${encodeURIComponent(proxy._extra)}`;
}
let mode = '';
if (proxy._mode) {
mode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
let vlessType = proxy.network;
if (
proxy.network === 'ws' &&
proxy['ws-opts']?.['v2ray-http-upgrade']
) {
vlessType = 'httpupgrade';
}
let vlessTransport = `&type=${encodeURIComponent(vlessType)}`;
if (['grpc'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
const authority =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
if (authority) {
vlessTransport += `&authority=${encodeURIComponent(
authority,
)}`;
}
}
let vlessTransportServiceName =
@@ -199,29 +287,55 @@ export default function URI_Producer() {
vlessTransportServiceName,
)}`;
}
if (proxy.network === 'kcp') {
if (proxy.seed) {
vlessTransport += `&seed=${encodeURIComponent(
proxy.seed,
)}`;
}
if (proxy.headerType) {
vlessTransport += `&headerType=${encodeURIComponent(
proxy.headerType,
)}`;
}
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?security=${encodeURIComponent(
security,
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'trojan':
let trojanTransport = '';
if (proxy.network) {
trojanTransport = `&type=${proxy.network}`;
let trojanType = proxy.network;
if (
proxy.network === 'ws' &&
proxy['ws-opts']?.['v2ray-http-upgrade']
) {
trojanType = 'httpupgrade';
}
trojanTransport = `&type=${encodeURIComponent(trojanType)}`;
if (['grpc'].includes(proxy.network)) {
let trojanTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let trojanTransportAuthority =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
if (trojanTransportAuthority) {
trojanTransport += `&authority=${encodeURIComponent(
trojanTransportAuthority,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
@@ -246,14 +360,68 @@ export default function URI_Producer() {
)}`;
}
}
let trojanFp = '';
if (proxy['client-fingerprint']) {
trojanFp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let trojanAlpn = '';
if (proxy.alpn) {
trojanAlpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
const trojanIsReality = proxy['reality-opts'];
let trojanSid = '';
let trojanPbk = '';
let trojanSpx = '';
let trojanSecurity = '';
let trojanMode = '';
let trojanExtra = '';
if (trojanIsReality) {
trojanSecurity = `&security=reality`;
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
trojanSid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;
}
if (proxy._extra) {
trojanExtra = `&extra=${encodeURIComponent(
proxy._extra,
)}`;
}
if (proxy._mode) {
trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
}${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'hysteria2':
let hysteria2params = [];
if (proxy['hop-interval']) {
hysteria2params.push(
`hop-interval=${proxy['hop-interval']}`,
);
}
if (proxy['keepalive']) {
hysteria2params.push(`keepalive=${proxy['keepalive']}`);
}
if (proxy['skip-cert-verify']) {
hysteria2params.push(`insecure=1`);
}
@@ -274,6 +442,9 @@ export default function URI_Producer() {
`sni=${encodeURIComponent(proxy.sni)}`,
);
}
if (proxy.ports) {
hysteria2params.push(`mport=${proxy.ports}`);
}
if (proxy['tls-fingerprint']) {
hysteria2params.push(
`pinSHA256=${encodeURIComponent(
@@ -330,7 +501,7 @@ export default function URI_Producer() {
hysteriaParams.push(`obfsParam=${proxy[key]}`);
} else if (['sni'].includes(key)) {
hysteriaParams.push(`peer=${proxy[key]}`);
} else if (proxy[key]) {
} else if (proxy[key] && !/^_/i.test(key)) {
hysteriaParams.push(
`${i}=${encodeURIComponent(proxy[key])}`,
);
@@ -357,6 +528,7 @@ export default function URI_Producer() {
'password',
'server',
'port',
'tls',
].includes(key)
) {
const i = key.replace(/-/, '_');
@@ -385,10 +557,19 @@ export default function URI_Producer() {
['disable-sni', 'reduce-rtt'].includes(key) &&
proxy[key]
) {
tuicParams.push(`${i}=1`);
} else if (proxy[key]) {
tuicParams.push(`${i.replace(/-/g, '_')}=1`);
} else if (
['congestion-controller'].includes(key)
) {
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])}`,
);
}
}
@@ -401,8 +582,99 @@ export default function URI_Producer() {
}?${tuicParams.join('&')}#${encodeURIComponent(
proxy.name,
)}`;
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':
let wireguardParams = [];
Object.keys(proxy).forEach((key) => {
if (
![
'name',
'type',
'server',
'port',
'ip',
'ipv6',
'private-key',
].includes(key)
) {
if (['public-key'].includes(key)) {
wireguardParams.push(`publickey=${proxy[key]}`);
} else if (['udp'].includes(key)) {
if (proxy[key]) {
wireguardParams.push(`${key}=1`);
}
} else if (proxy[key] && !/^_/i.test(key)) {
wireguardParams.push(
`${key}=${encodeURIComponent(proxy[key])}`,
);
}
}
});
if (proxy.ip && proxy.ipv6) {
wireguardParams.push(
`address=${proxy.ip}/32,${proxy.ipv6}/128`,
);
} else if (proxy.ip) {
wireguardParams.push(`address=${proxy.ip}/32`);
} else if (proxy.ipv6) {
wireguardParams.push(`address=${proxy.ipv6}/128`);
}
result = `wireguard://${encodeURIComponent(
proxy['private-key'],
)}@${proxy.server}:${proxy.port}/?${wireguardParams.join(
'&',
)}#${encodeURIComponent(proxy.name)}`;
break;
}
return result;
};

View File

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

View File

@@ -10,6 +10,8 @@ const RULE_TYPES_MAPPING = [
[/^PROTOCOL$/, 'PROTOCOL'],
[/^IP-CIDR$/i, 'IP-CIDR'],
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
[/^GEOIP$/i, 'GEOIP'],
[/^GEOSITE$/i, 'GEOSITE'],
];
function AllRuleParser() {
@@ -37,8 +39,7 @@ function AllRuleParser() {
content: params[1],
};
if (
rule.type === 'IP-CIDR' ||
rule.type === 'IP-CIDR6'
['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)
) {
rule.options = params.slice(2);
}

View File

@@ -10,6 +10,8 @@ function QXFilter() {
'SRC-IP',
'IN-PORT',
'PROTOCOL',
'GEOSITE',
'GEOIP',
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
@@ -29,6 +31,8 @@ function QXFilter() {
function SurgeRuleSet() {
const type = 'SINGLE';
const func = (rule) => {
const UNSUPPORTED = ['GEOSITE', 'GEOIP'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
let output = `${rule.type},${rule.content}`;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
output +=
@@ -43,7 +47,7 @@ function LoonRules() {
const type = 'SINGLE';
const func = (rule) => {
// skip unsupported rules
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
// Loon only supports the no-resolve option
@@ -69,7 +73,7 @@ function ClashRuleProvider() {
let output = `${TRANSFORM[rule.type] || rule.type},${
rule.content
}`;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
if (rule.options) {
// Clash only supports the no-resolve option
rule.options = rule.options.filter((option) =>

View File

@@ -11,6 +11,7 @@
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/
import { version } from '../package.json';
import $ from '@/core/app';
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
@@ -18,7 +19,6 @@ console.log(
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
import migrate from '@/utils/migration';
import serve from '@/restful';

View File

@@ -11,17 +11,65 @@ import { syncToGist } from '@/restful/artifacts';
import { findByName } from '@/utils/database';
!(async function () {
const settings = $.read(SETTINGS_KEY);
// if GitHub token is not configured
if (!settings.githubUser || !settings.gistToken) return;
let arg;
if (typeof $argument != 'undefined') {
arg = Object.fromEntries(
// eslint-disable-next-line no-undef
$argument.split('&').map((item) => item.split('=')),
);
} else {
arg = {};
}
let sub_names = (arg?.subscription ?? arg?.sub ?? '')
.split(/,|/g)
.map((i) => i.trim())
.filter((i) => i.length > 0)
.map((i) => decodeURIComponent(i));
let col_names = (arg?.collection ?? arg?.col ?? '')
.split(/,|/g)
.map((i) => i.trim())
.filter((i) => i.length > 0)
.map((i) => decodeURIComponent(i));
if (sub_names.length > 0 || col_names.length > 0) {
if (sub_names.length > 0)
await produceArtifacts(sub_names, 'subscription');
if (col_names.length > 0)
await produceArtifacts(col_names, 'collection');
} else {
const settings = $.read(SETTINGS_KEY);
// if GitHub token is not configured
if (!settings.githubUser || !settings.gistToken) return;
const artifacts = $.read(ARTIFACTS_KEY);
if (!artifacts || artifacts.length === 0) return;
const artifacts = $.read(ARTIFACTS_KEY);
if (!artifacts || artifacts.length === 0) return;
const shouldSync = artifacts.some((artifact) => artifact.sync);
if (shouldSync) await doSync();
const shouldSync = artifacts.some((artifact) => artifact.sync);
if (shouldSync) await doSync();
}
})().finally(() => $.done());
async function produceArtifacts(names, type) {
try {
if (names.length > 0) {
$.info(`produceArtifacts ${type} 开始: ${names.join(', ')}`);
await Promise.all(
names.map(async (name) => {
try {
await produceArtifact({
type,
name,
});
} catch (e) {
$.error(`${type} ${name} error: ${e.message ?? e}`);
}
}),
);
$.info(`produceArtifacts ${type} 完成: ${names.join(', ')}`);
}
} catch (e) {
$.error(`produceArtifacts error: ${e.message ?? e}`);
}
}
async function doSync() {
console.log(
`
@@ -36,12 +84,15 @@ async function doSync() {
const files = {};
try {
const valid = [];
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
@@ -62,6 +113,13 @@ async function doSync() {
}
});
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
@@ -69,6 +127,7 @@ async function doSync() {
await produceArtifact({
type: 'subscription',
name: subName,
awaitCustomCache: true,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
@@ -81,6 +140,15 @@ async function doSync() {
try {
if (artifact.sync && artifact.source) {
$.info(`正在同步云配置:${artifact.name}...`);
const useMihomoExternal =
artifact.platform === 'SurgeMac';
if (useMihomoExternal) {
$.info(
`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
);
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
@@ -88,6 +156,7 @@ async function doSync() {
produceOpts: {
'include-unsupported-proxy':
artifact.includeUnsupportedProxy,
useMihomoExternal,
},
});
@@ -97,27 +166,46 @@ async function doSync() {
files[encodeURIComponent(artifact.name)] = {
content: output,
};
valid.push(artifact.name);
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
`生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
);
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(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) {
if (artifact.sync) {
if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
@@ -128,17 +216,35 @@ async function doSync() {
files.map((item) => [item.path, item]),
);
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
const raw_url =
files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.notify('🌍 Sub-Store', '全部订阅同步成功');
$.info('上传配置成功');
if (invalid.length > 0) {
$.notify(
'🌍 Sub-Store',
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.notify('🌍 Sub-Store', '同步配置完成');
}
} catch (e) {
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${e.message ?? e}`);
$.error(`无法同步订阅配置到 Gist原因${e}`);
$.notify('🌍 Sub-Store', '同步配置失败', `原因:${e.message ?? e}`);
$.error(`无法同步配置到 Gist原因${e}`);
}
}

View File

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

View File

@@ -21,6 +21,7 @@ import registerSettingRoutes from '@/restful/settings';
import registerMiscRoutes from '@/restful/miscs';
import registerSortRoutes from '@/restful/sort';
import registerFileRoutes from '@/restful/file';
import registerTokenRoutes from '@/restful/token';
import registerModuleRoutes from '@/restful/module';
migrate();
@@ -32,6 +33,7 @@ function serve() {
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
registerTokenRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerArtifactRoutes($app);

View File

@@ -253,7 +253,25 @@ async function syncToGist(files) {
key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
});
return manager.upload(files);
const res = await manager.upload(files);
let body = {};
try {
body = JSON.parse(res.body);
// eslint-disable-next-line no-empty
} catch (e) {}
const url = body?.html_url ?? body?.web_url;
const settings = $.read(SETTINGS_KEY);
if (url) {
$.log(`同步 Gist 后, 找到 Sub-Store Gist: ${url}`);
settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID';
} else {
$.error(`同步 Gist 后, 找不到 Sub-Store Gist`);
settings.artifactStoreStatus = 'NOT FOUND';
}
$.write(settings, SETTINGS_KEY);
return res;
}
export { syncToGist };

View File

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

View File

@@ -1,25 +1,104 @@
import { getPlatformFromHeaders } from '@/utils/platform';
import {
getPlatformFromHeaders,
shouldIncludeUnsupportedProxy,
} from '@/utils/user-agent';
import { ProxyUtils } from '@/core/proxy-utils';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import $ from '@/core/app';
import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
import { produceArtifact } from '@/restful/sync';
// eslint-disable-next-line no-unused-vars
import { isIPv4, isIPv6 } from '@/utils';
import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) {
$app.get('/share/col/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/download/:name', downloadSubscription);
$app.get(
'/download/collection/:name/api/v1/server/details',
async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha';
await downloadCollection(req, res);
},
);
$app.get('/download/:name/api/v1/server/details', async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha';
await downloadSubscription(req, res);
});
$app.get(
'/download/collection/:name/api/v1/monitor/:nezhaIndex',
async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha-monitor';
await downloadCollection(req, res);
},
);
$app.get('/download/:name/api/v1/monitor/:nezhaIndex', async (req, res) => {
req.query.platform = 'JSON';
req.query.produceType = 'internal';
req.query.resultFormat = 'nezha-monitor';
await downloadSubscription(req, res);
});
}
async function downloadSubscription(req, res) {
let { name } = req.params;
let { name, nezhaIndex } = req.params;
name = decodeURIComponent(name);
nezhaIndex = decodeURIComponent(nezhaIndex);
const useMihomoExternal = req.query.target === 'SurgeMac';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
let {
url,
ua,
@@ -28,19 +107,59 @@ async function downloadSubscription(req, res) {
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
if (!/^https?:\/\//.test(url)) {
content = url;
$.info(`URL 不是链接,视为本地订阅`);
}
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
@@ -55,14 +174,41 @@ async function downloadSubscription(req, res) {
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
$.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) {
try {
const output = await produceArtifact({
const passThroughUA = sub.passThroughUA;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
);
ua = reqUA;
}
let output = await produceArtifact({
type: 'subscription',
name,
platform,
@@ -74,15 +220,60 @@ async function downloadSubscription(req, res) {
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
useMihomoExternal,
},
$options,
proxy,
noCache,
});
if (sub.source !== 'local' || url) {
let flowInfo;
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
// forward flow headers
const flowInfo = await getFlowHeaders(url || sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
url =
`${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow && /^https?/.test(url)) {
// forward flow headers
flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy,
$arguments.flowUrl,
);
if (flowInfo) {
res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
}
}
} catch (err) {
$.error(
@@ -92,8 +283,48 @@ async function downloadSubscription(req, res) {
);
}
}
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
res.set(
'subscription-userinfo',
normalizeFlowHeader(
[subUserInfo, flowInfo].filter((i) => i).join(';'),
),
);
}
if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);
} else if (resultFormat === 'nezha-monitor') {
nezhaIndex = /^\d+$/.test(nezhaIndex)
? parseInt(nezhaIndex, 10)
: output.findIndex((i) => i.name === nezhaIndex);
output = await nezhaMonitor(
output[nezhaIndex],
nezhaIndex,
req.query,
);
}
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
@@ -117,7 +348,7 @@ async function downloadSubscription(req, res) {
);
}
} else {
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}`);
$.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -130,19 +361,64 @@ async function downloadSubscription(req, res) {
}
async function downloadCollection(req, res) {
let { name } = req.params;
let { name, nezhaIndex } = req.params;
name = decodeURIComponent(name);
nezhaIndex = decodeURIComponent(nezhaIndex);
const useMihomoExternal = req.query.target === 'SurgeMac';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
$.info(`正在下载组合订阅:${name}`);
let {
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } =
req.query;
let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
@@ -155,12 +431,29 @@ async function downloadCollection(req, res) {
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
$.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
if (collection) {
try {
const output = await produceArtifact({
let output = await produceArtifact({
type: 'collection',
name,
platform,
@@ -168,19 +461,59 @@ async function downloadCollection(req, res) {
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
useMihomoExternal,
},
$options,
proxy,
noCache,
ua: reqUA,
});
let subUserInfoOfSub;
// forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY);
const subnames = collection.subscriptions;
if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') {
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
let url =
`${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow && /^https?:/.test(url)) {
subUserInfoOfSub = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy || collection.proxy,
$arguments.flowUrl,
);
}
} catch (err) {
$.error(
@@ -190,9 +523,77 @@ async function downloadCollection(req, res) {
);
}
}
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`组合订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
}
}
$.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`);
let subUserInfoOfCol;
if (/^https?:\/\//.test(collection.subUserinfo)) {
try {
subUserInfoOfCol = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || collection.proxy,
collection.subUserinfo,
);
} catch (e) {
$.error(
`组合订阅 ${name} 使用自定义流量链接 ${
collection.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfoOfCol = collection.subUserinfo;
}
const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
if (subUserInfo) {
res.set(
'subscription-userinfo',
normalizeFlowHeader(subUserInfo),
);
}
if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);
} else if (resultFormat === 'nezha-monitor') {
nezhaIndex = /^\d+$/.test(nezhaIndex)
? parseInt(nezhaIndex, 10)
: output.findIndex((i) => i.name === nezhaIndex);
output = await nezhaMonitor(
output[nezhaIndex],
nezhaIndex,
req.query,
);
}
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
@@ -215,7 +616,7 @@ async function downloadCollection(req, res) {
);
}
} else {
$.notify(
$.error(
`🌍 Sub-Store 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
@@ -229,3 +630,149 @@ async function downloadCollection(req, res) {
);
}
}
async function nezhaMonitor(proxy, index, query) {
const result = {
code: 0,
message: 'success',
result: [],
};
try {
const { isLoon, isSurge } = $.env;
if (!isLoon && !isSurge)
throw new Error('仅支持 Loon 和 Surge(ability=http-client-policy)');
const node = ProxyUtils.produce([proxy], isLoon ? 'Loon' : 'Surge');
if (!node) throw new Error('当前客户端不兼容此节点');
const monitors = proxy._monitors || [
{
name: 'Cloudflare',
url: 'http://cp.cloudflare.com/generate_204',
method: 'HEAD',
number: 3,
timeout: 2000,
},
{
name: 'Google',
url: 'http://www.google.com/generate_204',
method: 'HEAD',
number: 3,
timeout: 2000,
},
];
const number =
query.number || Math.max(...monitors.map((i) => i.number)) || 3;
for (const monitor of monitors) {
const interval = 10 * 60 * 1000;
const data = {
monitor_id: monitors.indexOf(monitor),
server_id: index,
monitor_name: monitor.name,
server_name: proxy.name,
created_at: [],
avg_delay: [],
};
for (let index = 0; index < number; index++) {
const startedAt = Date.now();
try {
await $.http[(monitor.method || 'HEAD').toLowerCase()]({
timeout: monitor.timeout || 2000,
url: monitor.url,
'policy-descriptor': node,
node,
});
const latency = Date.now() - startedAt;
$.info(`${monitor.name} latency: ${latency}`);
data.avg_delay.push(latency);
} catch (e) {
$.error(e);
data.avg_delay.push(0);
}
data.created_at.push(
Date.now() - interval * (monitor.number - index - 1),
);
}
result.result.push(data);
}
} catch (e) {
$.error(e);
result.result.push({
monitor_id: 0,
server_id: 0,
monitor_name: `${e.message ?? e}`,
server_name: proxy.name,
created_at: [Date.now()],
avg_delay: [0],
});
}
return JSON.stringify(result, null, 2);
}
function nezhaTransform(output) {
const result = {
code: 0,
message: 'success',
result: [],
};
output.map((proxy, index) => {
// 如果节点上有数据 就取节点上的数据
let CountryCode = proxy._geo?.countryCode || proxy._geo?.country;
// 简单判断下
if (!/^[a-z]{2}$/i.test(CountryCode)) {
CountryCode = getISO(proxy.name);
}
// 简单判断下
if (/^[a-z]{2}$/i.test(CountryCode)) {
// 如果节点上有数据 就取节点上的数据
let now = Math.round(new Date().getTime() / 1000);
let time = proxy._unavailable ? 0 : now;
const uptime = parseInt(proxy._uptime || 0, 10);
result.result.push({
id: index,
name: proxy.name,
tag: `${proxy._tag ?? ''}`,
last_active: time,
// 暂时不用处理 现在 VPings App 端的接口支持域名查询
// 其他场景使用 自己在 Sub-Store 加一步域名解析
valid_ip: proxy._IP || proxy.server,
ipv4: proxy._IPv4 || proxy.server,
ipv6: proxy._IPv6 || (isIPv6(proxy.server) ? proxy.server : ''),
host: {
Platform: 'Sub-Store',
PlatformVersion: env.version,
CPU: [],
MemTotal: 1024,
DiskTotal: 1024,
SwapTotal: 1024,
Arch: '',
Virtualization: '',
BootTime: now - uptime,
CountryCode, // 目前需要
Version: '0.0.1',
},
status: {
CPU: 0,
MemUsed: 0,
SwapUsed: 0,
DiskUsed: 0,
NetInTransfer: 0,
NetOutTransfer: 0,
NetInSpeed: 0,
NetOutSpeed: 0,
Uptime: uptime,
Load1: 0,
Load5: 0,
Load15: 0,
TcpConnCount: 0,
UdpConnCount: 0,
ProcessCount: 0,
},
});
}
});
return JSON.stringify(result, null, 2);
}

View File

@@ -1,5 +1,6 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import { FILES_KEY, ARTIFACTS_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import {
@@ -8,10 +9,13 @@ import {
InternalServerError,
} from '@/restful/errors';
import { produceArtifact } from '@/restful/sync';
import { formatDateTime } from '@/utils';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.get('/share/file/:name', getFile);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
@@ -48,17 +52,68 @@ function createFile(req, res) {
async function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(`正在下载文件:${name}\n请求 User-Agent: ${reqUA}`);
let {
url,
subInfoUrl,
subInfoUserAgent,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
proxy,
noCache,
produceType,
} = req.query;
let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`);
}
if (subInfoUrl) {
subInfoUrl = decodeURIComponent(subInfoUrl);
$.info(`指定获取流量的 subInfoUrl: ${subInfoUrl}`);
}
if (subInfoUserAgent) {
subInfoUserAgent = decodeURIComponent(subInfoUserAgent);
$.info(`指定获取流量的 subInfoUserAgent: ${subInfoUserAgent}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地文件: ${content}`);
@@ -71,6 +126,13 @@ async function getFile(req, res) {
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
@@ -84,11 +146,54 @@ async function getFile(req, res) {
content,
mergeSources,
ignoreFailedRemoteFile,
$options,
proxy,
noCache,
produceType,
all: true,
});
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);
try {
subInfoUrl = subInfoUrl || file.subInfoUrl;
if (subInfoUrl) {
// forward flow headers
const flowInfo = await getFlowHeaders(
subInfoUrl,
subInfoUserAgent || file.subInfoUserAgent,
undefined,
proxy || file.proxy,
);
if (flowInfo) {
res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
}
}
} catch (err) {
$.error(
`文件 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
if (file.download) {
res.set(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(
file.displayName || file.name,
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8');
if (output?.$options?._res?.headers) {
Object.entries(output.$options._res.headers).forEach(
([key, value]) => {
res.set(key, value);
},
);
}
res.send(output?.$content ?? '');
} catch (err) {
$.notify(
`🌍 Sub-Store 下载文件失败`,
@@ -106,7 +211,7 @@ async function getFile(req, res) {
);
}
} else {
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}`);
$.error(`🌍 Sub-Store 下载文件失败\n❌ 未找到文件:${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -119,11 +224,25 @@ async function getFile(req, res) {
}
function getWholeFile(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
success(res, file);
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(
`sub-store_file_${name}_${formatDateTime(
new Date(),
)}.json`,
)}"`,
)
.send(JSON.stringify(file));
} else {
success(res, file);
}
} else {
failed(
res,
@@ -149,6 +268,20 @@ function updateFile(req, res) {
};
$.info(`正在更新文件:${name}...`);
if (name !== newFile.name) {
// update all artifacts referring this collection
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
for (const artifact of allArtifacts) {
if (
artifact.type === 'file' &&
artifact.source === oldFile.name
) {
artifact.source = newFile.name;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
}
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);

View File

@@ -1,13 +1,18 @@
import { Base64 } from 'js-base64';
import _ from 'lodash';
import express from '@/vendor/express';
import $ from '@/core/app';
import migrate from '@/utils/migration';
import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync';
import download, { downloadFile } from '@/utils/download';
import { syncArtifacts, produceArtifact } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerTokenRoutes from './token';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download';
@@ -26,6 +31,77 @@ export default function serve() {
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
}
const $app = express({ substore: $, port, host });
if ($.env.isNode) {
const be_merge = eval('process.env.SUB_STORE_BACKEND_MERGE');
const be_prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
if (be_prefix || be_merge) {
if (!fe_be_path.startsWith('/')) {
throw new Error(
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
);
}
if (be_merge) {
$.info(`[BACKEND] MERGE mode is [ON].`);
$.info(`[BACKEND && FRONTEND] ${host}:${port}`);
}
$.info(`[BACKEND PREFIX] ${host}:${port}${fe_be_path}`);
$app.use((req, res, next) => {
if (req.path.startsWith(fe_be_path)) {
req.url = req.url.replace(fe_be_path, '') || '/';
if (be_merge && req.url.startsWith('/api/')) {
req.query['share'] = 'true';
}
next();
return;
}
const pathname =
decodeURIComponent(req._parsedUrl.pathname) || '/';
if (
be_merge &&
req.path.startsWith('/share/') &&
req.query.token
) {
if (req.method.toLowerCase() !== 'get') {
res.status(405).send('Method not allowed');
return;
}
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
`/share/${t.type}/${t.name}` === pathname &&
(t.exp == null || t.exp > Date.now()),
);
if (token) {
next();
return;
}
}
if (be_merge && fe_path && req.path.indexOf('/', 1) == -1) {
if (req.path.indexOf('.') == -1) {
req.url = '/index.html';
}
const express_ = eval(`require("express")`);
const mime_ = eval(`require("mime-types")`);
const path_ = eval(`require("path")`);
const staticFileMiddleware = express_.static(fe_path, {
setHeaders: (res, path) => {
const type = mime_.contentType(path_.extname(path));
if (type) {
res.set('Content-Type', type);
}
},
});
staticFileMiddleware(req, res, next);
return;
}
res.status(403).end('Forbbiden');
return;
});
}
}
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
@@ -35,6 +111,7 @@ export default function serve() {
registerSettingRoutes($app);
registerArtifactRoutes($app);
registerFileRoutes($app);
registerTokenRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app);
registerNodeInfoRoutes($app);
@@ -44,20 +121,180 @@ export default function serve() {
$app.start();
if ($.env.isNode) {
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON');
if (backend_cron) {
$.info(`[CRON] ${backend_cron} enabled`);
// Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON
const backend_sync_cron = eval(
'process.env.SUB_STORE_BACKEND_SYNC_CRON',
);
if (backend_sync_cron) {
$.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_cron,
backend_sync_cron,
async function () {
try {
$.info(`[CRON] ${backend_cron} started`);
$.info(`[SYNC CRON] ${backend_sync_cron} started`);
await syncArtifacts();
$.info(`[CRON] ${backend_cron} finished`);
$.info(`[SYNC CRON] ${backend_sync_cron} finished`);
} catch (e) {
$.error(
`[CRON] ${backend_cron} error: ${e.message ?? e}`,
`[SYNC CRON] ${backend_sync_cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
} else {
if (eval('process.env.SUB_STORE_BACKEND_CRON')) {
$.error(
`[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
);
}
if (eval('process.env.SUB_STORE_CRON')) {
$.error(
`[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
);
}
}
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
if (produce_cron) {
$.info(`[PRODUCE CRON] ${produce_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
produce_cron.split(/\s*;\s*/).map((item) => {
const [cron, type, name] = item.split(/\s*,\s*/);
new CronJob(
cron.trim(),
async function () {
try {
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} started`,
);
await produceArtifact({ type, name });
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} finished`,
);
} catch (e) {
$.error(
`[PRODUCE CRON] ${type} ${name} ${cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
});
}
const backend_download_cron = eval(
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
);
if (backend_download_cron) {
$.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_download_cron,
async function () {
try {
$.info(
`[DOWNLOAD CRON] ${backend_download_cron} started`,
);
await gistBackupAction('download');
$.info(
`[DOWNLOAD CRON] ${backend_download_cron} finished`,
);
} catch (e) {
$.error(
`[DOWNLOAD CRON] ${backend_download_cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const backend_upload_cron = eval(
'process.env.SUB_STORE_BACKEND_UPLOAD_CRON',
);
if (backend_upload_cron) {
$.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_upload_cron,
async function () {
try {
$.info(`[UPLOAD CRON] ${backend_upload_cron} started`);
await gistBackupAction('upload');
$.info(`[UPLOAD CRON] ${backend_upload_cron} finished`);
} catch (e) {
$.error(
`[UPLOAD CRON] ${backend_upload_cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const mmdb_cron = eval('process.env.SUB_STORE_MMDB_CRON');
const countryFile = eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
const countryUrl = eval('process.env.SUB_STORE_MMDB_COUNTRY_URL');
const asnFile = eval('process.env.SUB_STORE_MMDB_ASN_PATH');
const asnUrl = eval('process.env.SUB_STORE_MMDB_ASN_URL');
if (mmdb_cron && ((countryFile && countryUrl) || (asnFile && asnUrl))) {
$.info(`[MMDB CRON] ${mmdb_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
mmdb_cron,
async function () {
try {
$.info(`[MMDB CRON] ${mmdb_cron} started`);
if (countryFile && countryUrl) {
try {
$.info(
`[MMDB CRON] downloading ${countryUrl} to ${countryFile}`,
);
await downloadFile(countryUrl, countryFile);
} catch (e) {
$.error(
`[MMDB CRON] ${countryUrl} download failed: ${
e.message ?? e
}`,
);
}
}
if (asnFile && asnUrl) {
try {
$.info(
`[MMDB CRON] downloading ${asnUrl} to ${asnFile}`,
);
await downloadFile(asnUrl, asnFile);
} catch (e) {
$.error(
`[MMDB CRON] ${asnUrl} download failed: ${
e.message ?? e
}`,
);
}
}
$.info(`[MMDB CRON] ${mmdb_cron} finished`);
} catch (e) {
$.error(
`[MMDB CRON] ${mmdb_cron} error: ${e.message ?? e}`,
);
}
}, // onTick
@@ -69,6 +306,7 @@ export default function serve() {
const path = eval(`require("path")`);
const fs = eval(`require("fs")`);
const data_url = eval('process.env.SUB_STORE_DATA_URL');
const data_url_post = eval('process.env.SUB_STORE_DATA_URL_POST');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
const fe_host =
@@ -77,11 +315,12 @@ export default function serve() {
const fe_abs_path = path.resolve(
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 {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {
throw new Error(
$.error(
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
);
}
@@ -96,10 +335,15 @@ export default function serve() {
const staticFileMiddleware = express_.static(fe_path);
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/';
let be_download = '/download/';
let be_share = '/share/';
let be_download_rewrite = '';
let be_api_rewrite = '';
let be_share_rewrite = `${be_share}:type/:name`;
let prefix = eval('process.env.SUB_STORE_BACKEND_PREFIX')
? fe_be_path
: '';
if (fe_be_path) {
if (!fe_be_path.startsWith('/')) {
throw new Error(
@@ -112,28 +356,44 @@ export default function serve() {
be_download_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_download}`;
app.use(
be_share_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}${prefix}`,
changeOrigin: true,
pathRewrite: async (path, req) => {
if (req.method.toLowerCase() !== 'get')
throw new Error('Method not allowed');
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
t.type === req.params.type &&
t.name === req.params.name &&
(t.exp == null || t.exp > Date.now()),
);
if (!token) throw new Error('Forbbiden');
return req.originalUrl;
},
}),
);
app.use(
be_api_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_api_rewrite)
? path.replace(be_api_rewrite, be_api)
: path;
target: `http://127.0.0.1:${port}${prefix}${be_api}`,
pathRewrite: async (path) => {
return path.includes('?')
? `${path}&share=true`
: `${path}?share=true`;
},
}),
);
app.use(
be_download_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
target: `http://127.0.0.1:${port}${prefix}${be_download}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_download_rewrite)
? path.replace(be_download_rewrite, be_download)
: path;
},
}),
);
}
@@ -153,10 +413,13 @@ export default function serve() {
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
if (fe_be_path) {
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> ${host}:${port}${prefix}${be_api}`,
);
$.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(
`[SHARE BACKEND] ${fe_address}:${fe_port}${be_share_rewrite}`,
);
}
});
@@ -164,10 +427,39 @@ export default function serve() {
if (data_url) {
$.info(`[BACKEND] downloading data from ${data_url}`);
download(data_url)
.then((content) => {
$.write(content, '#sub-store');
.then(async (content) => {
try {
content = JSON.parse(Base64.decode(content));
if (Object.keys(content.settings).length === 0) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
try {
content = JSON.parse(content);
if (Object.keys(content.settings).length === 0) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
}
if (data_url_post) {
$.info('[BACKEND] executing post-processing script');
eval(data_url_post);
}
$.cache = JSON.parse(content);
$.write(JSON.stringify(content, null, ` `), '#sub-store');
$.cache = content;
$.persistCache();
migrate();

View File

@@ -1,8 +1,12 @@
import { Base64 } from 'js-base64';
import _ from 'lodash';
import $ from '@/core/app';
import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
import { updateArtifactStore, updateAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import {
GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY,
@@ -12,6 +16,7 @@ import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist';
import migrate from '@/utils/migration';
import env from '@/utils/env';
import { formatDateTime } from '@/utils';
export default function register($app) {
// utils
@@ -25,7 +30,9 @@ export default function register($app) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
'attachment; filename="sub-store.json"',
`attachment; filename="${encodeURIComponent(
`sub-store_data_${formatDateTime(new Date())}.json`,
)}"`,
)
.send(
$.env.isNode
@@ -34,21 +41,47 @@ export default function register($app) {
);
})
.post((req, res) => {
const { content } = req.body;
$.write(content, '#sub-store');
let { content } = req.body;
try {
content = JSON.parse(Base64.decode(content));
if (Object.keys(content.settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
try {
content = JSON.parse(content);
if (Object.keys(content.settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
$.error(
`备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('备份文件校验失败, 无法还原');
}
}
$.write(JSON.stringify(content, null, ` `), '#sub-store');
if ($.env.isNode) {
$.cache = JSON.parse(content);
$.cache = content;
$.persistCache();
}
migrate();
success(res);
});
// Redirect sub.store to vercel webpage
$app.get('/', async (req, res) => {
// 302 redirect
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
});
if (ENV().isNode) {
$app.get('/', getEnv);
} else {
// Redirect sub.store to vercel webpage
$app.get('/', async (req, res) => {
// 302 redirect
res.set('location', 'https://sub-store.vercel.app/')
.status(302)
.end();
});
}
// handle preflight request for QX
if (ENV().isQX) {
@@ -63,7 +96,22 @@ export default function register($app) {
}
function getEnv(req, res) {
success(res, env);
if (req.query.share) {
env.feature.share = true;
}
res.set('Content-Type', 'application/json;charset=UTF-8').send(
JSON.stringify(
{
status: 'success',
data: {
guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',
...env,
},
},
null,
2,
),
);
}
async function refresh(_, res) {
@@ -73,13 +121,126 @@ async function refresh(_, res) {
// 2. clear resource cache
resourceCache.revokeAll();
scriptResourceCache.revokeAll();
headersResourceCache.revokeAll();
success(res);
}
async function gistBackup(req, res) {
const { action } = req.query;
async function gistBackupAction(action, keep, encode) {
// read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) throw new Error('GitHub Token is required for backup!');
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
let currentContent = $.read('#sub-store');
currentContent = currentContent ? JSON.parse(currentContent) : {};
if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache));
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
try {
content = $.read('#sub-store');
content = content ? JSON.parse(content) : {};
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
if (encode === 'plaintext') {
content.settings.gistToken =
'恢复后请重新设置 GitHub Token';
content = JSON.stringify(content, null, ` `);
} else {
content = Base64.encode(
JSON.stringify(content, null, ` `),
);
}
$.info(`下载备份, 与本地内容对比...`);
const onlineContent = await gist.download(
GIST_BACKUP_FILE_NAME,
);
if (onlineContent === content) {
$.info(`内容一致, 无需上传备份`);
return;
}
} catch (error) {
$.error(`${error.message ?? error}`);
}
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
content = content ? JSON.parse(content) : {};
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
if (encode === 'plaintext') {
content.settings.gistToken = '恢复后请重新设置 GitHub Token';
content = JSON.stringify(content, null, ` `);
} else {
content = Base64.encode(JSON.stringify(content, null, ` `));
}
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
$.info(`上传备份完成`);
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
content = JSON.parse(Base64.decode(content));
if (Object.keys(content.settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
try {
content = JSON.parse(content);
if (Object.keys(content.settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
}
if (keep) {
$.info(`保留原有设置 ${keep}`);
keep.split(',').forEach((path) => {
_.set(content, path, _.get(currentContent, path));
});
}
// restore settings
$.write(JSON.stringify(content, null, ` `), '#sub-store');
if ($.env.isNode) {
$.cache = content;
$.persistCache();
}
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
}
async function gistBackup(req, res) {
const { action, keep, encode } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
failed(
res,
@@ -89,78 +250,23 @@ async function gistBackup(req, res) {
),
);
} else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
try {
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (
Object.keys(JSON.parse(content).settings).length ===
0
) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
await gistBackupAction(action, keep, encode);
success(res);
} catch (err) {
$.error(
`Failed to ${action} gist data.\nReason: ${err.message ?? err}`,
);
failed(
res,
new InternalServerError(
'BACKUP_FAILED',
`Failed to ${action} data to gist!`,
`Failed to ${action} gist data!`,
`Reason: ${err.message ?? err}`,
),
);
}
}
}
export { gistBackupAction };

View File

@@ -15,46 +15,60 @@ export default function register($app) {
async function previewFile(req, res) {
try {
const file = req.body;
let content;
if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
content = file.content;
} else {
const errors = {};
content = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let content = '';
if (file.type !== 'mihomoProfile') {
if (
!file.ignoreFailedRemoteFile &&
Object.keys(errors).length > 0
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
content = file.content;
} else {
const errors = {};
content = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
file.ua,
undefined,
file.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
if (Object.keys(errors).length > 0) {
if (!file.ignoreFailedRemoteFile) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (file.ignoreFailedRemoteFile === 'enabled') {
$.notify(
`🌍 Sub-Store 预览文件失败`,
`${file.name}`,
`远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
}
}
}
// parse proxies
@@ -67,7 +81,7 @@ async function previewFile(req, res) {
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
{ $files: files, $content: filesContent, $file: file },
file.process,
)
: { $content: filesContent, $files: files };
@@ -109,7 +123,16 @@ async function compareSub(req, res) {
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
return await download(
url,
sub.ua,
undefined,
sub.proxy,
undefined,
undefined,
undefined,
true,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -120,12 +143,22 @@ async function compareSub(req, res) {
}),
);
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
} else if (sub.ignoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 预览订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content);
@@ -141,7 +174,8 @@ async function compareSub(req, res) {
// add id
original.forEach((proxy, i) => {
proxy.id = i;
proxy.subName = sub.name;
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
});
// apply processors
@@ -171,7 +205,20 @@ async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const subnames = [...collection.subscriptions];
let subscriptionTags = collection.subscriptionTags;
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
allSubs.forEach((sub) => {
if (
Array.isArray(sub.tag) &&
sub.tag.length > 0 &&
!subnames.includes(sub.name) &&
sub.tag.some((tag) => subscriptionTags.includes(tag))
) {
subnames.push(sub.name);
}
});
}
const results = {};
const errors = {};
await Promise.all(
@@ -195,7 +242,16 @@ async function compareCollection(req, res) {
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
return await download(
url,
sub.ua,
undefined,
sub.proxy,
undefined,
undefined,
undefined,
true,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -205,15 +261,25 @@ async function compareCollection(req, res) {
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (
sub.ignoreFailedRemoteSub === 'enabled'
) {
$.notify(
`🌍 Sub-Store 预览订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
@@ -227,8 +293,10 @@ async function compareCollection(req, res) {
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
});
// apply processors
@@ -243,24 +311,28 @@ async function compareCollection(req, res) {
errors[name] = err;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
`❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name} 时出现错误:${err}`,
);
}
}),
);
if (
!collection.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${collection.name} 中的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!collection.ignoreFailedRemoteSub) {
throw new Error(
`组合订阅 ${collection.name} 的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (collection.ignoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 预览组合订阅失败`,
`${collection.name}`,
`子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
// merge proxies with the original order
const original = Array.prototype.concat.apply(
@@ -270,7 +342,8 @@ async function compareCollection(req, res) {
original.forEach((proxy, i) => {
proxy.id = i;
proxy.collectionName = collection.name;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
});
const processed = await ProxyUtils.process(

View File

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

View File

@@ -5,10 +5,20 @@ import {
RequestInvalidError,
} from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow';
import {
SUBS_KEY,
COLLECTIONS_KEY,
ARTIFACTS_KEY,
FILES_KEY,
} from '@/constants';
import {
getFlowHeaders,
parseFlowHeaders,
getRmainingDays,
} from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
import { formatDateTime } from '@/utils';
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
@@ -30,6 +40,11 @@ export default function register($app) {
async function getFlowInfo(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let { url } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (!sub) {
@@ -43,20 +58,107 @@ async function getFlowInfo(req, res) {
);
return;
}
if (sub.source === 'local') {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Local subscription ${name} has no flow information!`,
),
);
if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
try {
success(res, {
...parseFlowHeaders(subUserInfo),
});
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
} else {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Local subscription ${name} has no flow information!`,
),
);
}
return;
}
try {
const flowHeaders = await getFlowHeaders(sub.url);
if (!flowHeaders) {
url =
`${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if ($arguments.noFlow || !/^https?/.test(url)) {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Subscription ${name}: noFlow`,
),
);
return;
}
const flowHeaders = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
);
if (!flowHeaders && !sub.subUserinfo) {
failed(
res,
new InternalServerError(
@@ -67,8 +169,56 @@ async function getFlowInfo(req, res) {
);
return;
}
success(res, parseFlowHeaders(flowHeaders));
try {
const remainingDays = getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
});
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
const result = {
...parseFlowHeaders(
[subUserInfo, flowHeaders].filter((i) => i).join('; '),
),
};
if (remainingDays != null) {
result.remainingDays = remainingDays;
}
success(res, result);
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
} catch (err) {
failed(
res,
@@ -111,11 +261,25 @@ function createSubscription(req, res) {
function getSubscription(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) {
success(res, sub);
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(
`sub-store_subscription_${name}_${formatDateTime(
new Date(),
)}.json`,
)}"`,
)
.send(JSON.stringify(sub));
} else {
success(res, sub);
}
} else {
failed(
res,
@@ -161,9 +325,20 @@ function updateSubscription(req, res) {
artifact.source = sub.name;
}
}
// update all files referring this subscription
const allFiles = $.read(FILES_KEY) || [];
for (const file of allFiles) {
if (
file.sourceType === 'subscription' &&
file.sourceName == name
) {
file.sourceName = sub.name;
}
}
$.write(allCols, COLLECTIONS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
}
updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY);

View File

@@ -35,13 +35,26 @@ async function produceArtifact({
ignoreFailedRemoteFile,
produceType,
produceOpts = {},
subscription,
awaitCustomCache,
$options,
proxy,
noCache,
all,
}) {
platform = platform || 'JSON';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (!sub) throw new Error(`找不到订阅 ${name}`);
if (['subscription', 'sub'].includes(type)) {
let sub;
if (name) {
const allSubs = $.read(SUBS_KEY);
sub = findByName(allSubs, name);
if (!sub) throw new Error(`找不到订阅 ${name}`);
} else if (subscription) {
sub = subscription;
} else {
throw new Error('未提供订阅名称或订阅数据');
}
let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
@@ -54,7 +67,16 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
return await download(
url,
ua || sub.ua,
undefined,
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache || sub.noCache,
true,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -68,12 +90,23 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!subIgnoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
} else if (subIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
@@ -94,7 +127,16 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || sub.ua);
return await download(
url,
ua || sub.ua,
undefined,
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache || sub.noCache,
true,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -108,12 +150,23 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!subIgnoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
} else if (subIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
@@ -121,13 +174,17 @@ async function produceArtifact({
raw.push(sub.content);
}
}
if (produceType === 'raw') {
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
}
// parse proxies
let proxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
proxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
});
// apply processors
proxies = await ProxyUtils.process(
@@ -135,6 +192,7 @@ async function produceArtifact({
sub.process || [],
platform,
{ [sub.name]: sub },
$options,
);
if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -158,12 +216,25 @@ async function produceArtifact({
}
// produce
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') {
} else if (['collection', 'col'].includes(type)) {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
if (!collection) throw new Error(`找不到组合订阅 ${name}`);
const subnames = collection.subscriptions;
const subnames = [...collection.subscriptions];
let subscriptionTags = collection.subscriptionTags;
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
allSubs.forEach((sub) => {
if (
Array.isArray(sub.tag) &&
sub.tag.length > 0 &&
!subnames.includes(sub.name) &&
sub.tag.some((tag) => subscriptionTags.includes(tag))
) {
subnames.push(sub.name);
}
});
}
const results = {};
const errors = {};
let processed = 0;
@@ -171,6 +242,14 @@ async function produceArtifact({
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
const passThroughUA = sub.passThroughUA;
let reqUA = sub.ua;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,
);
reqUA = ua;
}
try {
$.info(`正在处理子订阅:${sub.name}...`);
let raw;
@@ -190,7 +269,18 @@ async function produceArtifact({
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, sub.ua);
return await download(
url,
reqUA,
undefined,
proxy ||
sub.proxy ||
collection.proxy,
undefined,
undefined,
noCache || sub.noCache,
true,
);
} catch (err) {
errors[url] = err;
$.error(
@@ -200,15 +290,25 @@ async function produceArtifact({
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (
sub.ignoreFailedRemoteSub === 'enabled'
) {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
@@ -222,8 +322,10 @@ async function produceArtifact({
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
});
// apply processors
@@ -231,7 +333,11 @@ async function produceArtifact({
currentProxies,
sub.process || [],
platform,
{ [sub.name]: sub, _collection: collection },
{
[sub.name]: sub,
_collection: collection,
$options,
},
);
results[name] = currentProxies;
processed++;
@@ -257,15 +363,23 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (
!collectionIgnoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
if (Object.keys(errors).length > 0) {
if (!collectionIgnoreFailedRemoteSub) {
throw new Error(
`组合订阅 ${collection.name} 的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (collectionIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理组合订阅失败`,
`${collection.name}`,
`子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
// merge proxies with the original order
@@ -275,7 +389,8 @@ async function produceArtifact({
);
proxies.forEach((proxy) => {
proxy.collectionName = collection.name;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
});
// apply own processors
@@ -284,6 +399,7 @@ async function produceArtifact({
collection.process || [],
platform,
{ _collection: collection },
$options,
);
if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -338,90 +454,128 @@ async function produceArtifact({
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`);
let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
let raw = '';
if (file.type !== 'mihomoProfile') {
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
content &&
!['localFirst', 'remoteFirst'].includes(mergeSources)
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (
!fileIgnoreFailedRemoteFile &&
Object.keys(errors).length > 0
) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (Object.keys(errors).length > 0) {
if (!fileIgnoreFailedRemoteFile) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (fileIgnoreFailedRemoteFile === 'enabled') {
$.notify(
`🌍 Sub-Store 处理文件失败`,
`${file.name}`,
`远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
}
if (produceType === 'raw') {
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
let filesContent = files
@@ -432,12 +586,17 @@ async function produceArtifact({
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
{
$files: files,
$content: filesContent,
$options,
$file: file,
},
file.process,
)
: { $content: filesContent, $files: files };
: { $content: filesContent, $files: files, $options };
return processed?.$content ?? '';
return (all ? processed : processed?.$content) ?? '';
}
}
@@ -447,12 +606,15 @@ async function syncArtifacts() {
const files = {};
try {
const valid = [];
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
@@ -473,6 +635,13 @@ async function syncArtifacts() {
}
});
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
@@ -480,6 +649,7 @@ async function syncArtifacts() {
await produceArtifact({
type: 'subscription',
name: subName,
awaitCustomCache: true,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
@@ -493,6 +663,16 @@ async function syncArtifacts() {
try {
if (artifact.sync && artifact.source) {
$.info(`正在同步云配置:${artifact.name}...`);
const useMihomoExternal =
artifact.platform === 'SurgeMac';
if (useMihomoExternal) {
$.info(
`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
);
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
@@ -500,6 +680,7 @@ async function syncArtifacts() {
produceOpts: {
'include-unsupported-proxy':
artifact.includeUnsupportedProxy,
useMihomoExternal,
},
});
@@ -509,27 +690,47 @@ async function syncArtifacts() {
files[encodeURIComponent(artifact.name)] = {
content: output,
};
valid.push(artifact.name);
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
`生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
);
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(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) {
if (artifact.sync) {
if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
@@ -540,17 +741,34 @@ async function syncArtifacts() {
files.map((item) => [item.path, item]),
);
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
const raw_url =
files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功');
$.info('上传配置成功');
if (invalid.length > 0) {
throw new Error(
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.info(`同步配置成功 ${valid.length}`);
}
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
$.error(`同步配置失败,原因:${e.message ?? e}`);
throw e;
}
}
@@ -560,7 +778,7 @@ async function syncAllArtifacts(_, res) {
await syncArtifacts();
success(res);
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
$.error(`同步配置失败,原因:${e.message ?? e}`);
failed(
res,
new InternalServerError(
@@ -606,12 +824,18 @@ async function syncArtifact(req, res) {
}
try {
const useMihomoExternal = artifact.platform === 'SurgeMac';
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
produceOpts: {
'include-unsupported-proxy': artifact.includeUnsupportedProxy,
useMihomoExternal,
},
});
@@ -631,16 +855,34 @@ async function syncArtifact(req, res) {
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
let files = body.files;
let isGitLab;
if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(files.map((item) => [item.path, item]));
}
const url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab
? url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {

View File

@@ -0,0 +1,192 @@
import { deleteByName } from '@/utils/database';
import { ENV } from '@/vendor/open-api';
import { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, InternalServerError } from '@/restful/errors';
export default function register($app) {
if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY);
$app.post('/api/token', signToken);
$app.route('/api/token/:token').delete(deleteToken);
$app.route('/api/tokens').get(getAllTokens);
}
function deleteToken(req, res) {
let { token } = req.params;
token = decodeURIComponent(token);
$.info(`正在删除:${token}`);
let allTokens = $.read(TOKENS_KEY);
deleteByName(allTokens, token, 'token');
$.write(allTokens, TOKENS_KEY);
success(res);
}
function getAllTokens(req, res) {
const { type, name } = req.query;
const allTokens = $.read(TOKENS_KEY) || [];
success(
res,
type || name
? allTokens.filter(
(item) =>
(type ? item.type === type : true) &&
(name ? item.name === name : true),
)
: allTokens,
);
}
async function signToken(req, res) {
if (!ENV().isNode) {
return failed(
res,
new RequestInvalidError(
'INVALID_ENV',
`This endpoint is only available in Node.js environment`,
),
);
}
try {
const { payload, options } = req.body;
const ms = eval(`require("ms")`);
const type = payload?.type;
const name = payload?.name;
if (!type || !name)
return failed(
res,
new RequestInvalidError(
'INVALID_PAYLOAD',
`payload type and name are required`,
),
);
let token = payload?.token;
if (token != null) {
if (typeof token !== 'string' || token.length < 1) {
return failed(
res,
new RequestInvalidError(
'INVALID_CUSTOM_TOKEN',
`Invalid custom token: ${token}`,
),
);
}
const tokens = $.read(TOKENS_KEY) || [];
if (
tokens.find(
(t) =>
t.token === token && t.type === type && t.name === name,
)
) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_TOKEN',
`Token ${token} already exists`,
),
);
}
}
if (type === 'col') {
const collections = $.read(COLLECTIONS_KEY) || [];
const collection = collections.find((c) => c.name === name);
if (!collection)
return failed(
res,
new RequestInvalidError(
'INVALID_COLLECTION',
`collection ${name} not found`,
),
);
} else if (type === 'file') {
const files = $.read(FILES_KEY) || [];
const file = files.find((f) => f.name === name);
if (!file)
return failed(
res,
new RequestInvalidError(
'INVALID_FILE',
`file ${name} not found`,
),
);
} else if (type === 'sub') {
const subs = $.read(SUBS_KEY) || [];
const sub = subs.find((s) => s.name === name);
if (!sub)
return failed(
res,
new RequestInvalidError(
'INVALID_SUB',
`sub ${name} not found`,
),
);
} else {
return failed(
res,
new RequestInvalidError(
'INVALID_TYPE',
`type ${name} not supported`,
),
);
}
let expiresIn = options?.expiresIn;
if (options?.expiresIn != null) {
expiresIn = ms(options.expiresIn);
if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
return failed(
res,
new RequestInvalidError(
'INVALID_EXPIRES_IN',
`Invalid expiresIn option: ${options.expiresIn}`,
),
);
}
}
// const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const nanoid = eval(`require("nanoid")`);
const tokens = $.read(TOKENS_KEY) || [];
// const now = Date.now();
// for (const key in tokens) {
// const token = tokens[key];
// if (token.exp != null || token.exp < now) {
// delete tokens[key];
// }
// }
if (!token) {
do {
token = nanoid.customAlphabet(nanoid.urlAlphabet)();
} while (
tokens.find(
(t) =>
t.token === token && t.type === type && t.name === name,
)
);
}
tokens.push({
...payload,
token,
createdAt: Date.now(),
expiresIn: expiresIn > 0 ? options?.expiresIn : undefined,
exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,
});
$.write(tokens, TOKENS_KEY);
return success(res, {
token,
// secret,
});
} catch (e) {
return failed(
res,
new InternalServerError(
'TOKEN_SIGN_FAILED',
`Failed to sign token`,
`Reason: ${e.message ?? e}`,
),
);
}
}

View File

@@ -1,17 +1,17 @@
export function findByName(list, name) {
return list.find((item) => item.name === name);
export function findByName(list, name, field = 'name') {
return list.find((item) => item[field] === name);
}
export function findIndexByName(list, name) {
return list.findIndex((item) => item.name === name);
export function findIndexByName(list, name, field = 'name') {
return list.findIndex((item) => item[field] === name);
}
export function deleteByName(list, name) {
const idx = findIndexByName(list, name);
export function deleteByName(list, name, field = 'name') {
const idx = findIndexByName(list, name, field);
list.splice(idx, 1);
}
export function updateByName(list, name, newItem) {
const idx = findIndexByName(list, name);
export function updateByName(list, name, newItem, field = 'name') {
const idx = findIndexByName(list, name, field);
list[idx] = newItem;
}

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

@@ -0,0 +1,50 @@
import $ from '@/core/app';
import dnsPacket from 'dns-packet';
import { Buffer } from 'buffer';
import { isIPv4 } from '@/utils';
export async function doh({ url, domain, type = 'A', timeout, edns }) {
const buf = dnsPacket.encode({
type: 'query',
id: 0,
flags: dnsPacket.RECURSION_DESIRED,
questions: [
{
type,
name: domain,
},
],
additionals: [
{
type: 'OPT',
name: '.',
udpPayloadSize: 4096,
flags: 0,
options: [
{
code: 'CLIENT_SUBNET',
ip: edns,
sourcePrefixLength: isIPv4(edns) ? 24 : 56,
scopePrefixLength: 0,
},
],
},
],
});
const res = await $.http.get({
url: `${url}?dns=${buf
.toString('base64')
.toString('utf-8')
.replace(/=/g, '')}`,
headers: {
Accept: 'application/dns-message',
// 'Content-Type': 'application/dns-message',
},
// body: buf,
'binary-mode': true,
encoding: null, // 使用 null 编码以确保响应是原始二进制数据
timeout,
});
return dnsPacket.decode(Buffer.from($.env.isQX ? res.bodyBytes : res.body));
}

View File

@@ -1,6 +1,7 @@
import { SETTINGS_KEY } from '@/constants';
import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import resourceCache from '@/utils/resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import {
@@ -10,10 +11,27 @@ import {
validCheck,
} from '@/utils/flow';
import $ from '@/core/app';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
import { ProxyUtils } from '@/core/proxy-utils';
const clashPreprocessor = PROXY_PREPROCESSORS.find(
(processor) => processor.name === 'Clash Pre-processor',
);
const tasks = new Map();
export default async function download(rawUrl, ua, timeout) {
export default async function download(
rawUrl = '',
ua,
timeout,
customProxy,
skipCustomCache,
awaitCustomCache,
noCache,
preprocess,
) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
const rawArgs = url.split('#');
@@ -34,29 +52,136 @@ export default async function download(rawUrl, ua, timeout) {
}
}
}
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
// if (downloadUrlMatch) {
// let type = downloadUrlMatch?.[1];
// let name = downloadUrlMatch?.[2];
// if (name == null) {
// throw new Error(`本地 ${type} URL 无效: ${url}`);
// }
// name = decodeURIComponent(name);
// const key = type === 'module' ? MODULES_KEY : FILES_KEY;
// const item = findByName($.read(key), name);
// if (!item) {
// throw new Error(`找不到本地 ${type}: ${name}`);
// }
// return item.content;
// }
const { isNode } = ENV();
const { defaultUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const {
defaultProxy,
defaultUserAgent,
defaultTimeout,
cacheThreshold: defaultCacheThreshold,
} = $.read(SETTINGS_KEY);
const cacheThreshold = defaultCacheThreshold || 1024;
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
if ($arguments?.cacheKey === true) {
$.error(`使用自定义缓存时 cacheKey 的值不能为空`);
$arguments.cacheKey = undefined;
}
const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined;
if (customCacheKey && !skipCustomCache) {
const customCached = $.read(customCacheKey);
const cached = resourceCache.get(id);
if (!noCache && !$arguments?.noCache && cached) {
$.info(
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
);
return cached;
}
if (customCached) {
if (awaitCustomCache) {
$.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
try {
await download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
undefined,
undefined,
preprocess,
);
} catch (e) {
$.error(
`乐观缓存: URL ${url} 更新缓存发生错误 ${
e.message ?? e
}`,
);
$.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
resourceCache.set(id, customCached);
}
} else {
$.info(
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
);
download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
undefined,
undefined,
preprocess,
).catch((e) => {
$.error(
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
e.message ?? e
}`,
);
});
}
return customCached;
}
}
const downloadUrlMatch = url
.split('#')[0]
.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = '';
try {
type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到 ${type}: ${name}`);
}
if (type === 'module') {
return item.content;
} else {
return await produceArtifact({
type: 'file',
name,
});
}
} catch (err) {
$.error(
`Error when loading ${type}: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
return fs.readFileSync(url.split('#')[0], 'utf8');
} catch (err) {
$.error(
`Error when reading local file: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取文本内容: ${url}`);
}
}
if (!isNode && tasks.has(id)) {
return tasks.get(id);
}
@@ -64,6 +189,10 @@ export default async function download(rawUrl, ua, timeout) {
const http = HTTP({
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
: {}),
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
},
timeout: requestTimeout,
});
@@ -72,28 +201,112 @@ export default async function download(rawUrl, ua, timeout) {
// try to find in app cache
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}`);
if (!noCache && !$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}, ${userAgent}`);
result = cached;
if (customCacheKey) {
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
$.write(cached, customCacheKey);
}
} else {
const insecure = $arguments?.insecure
? isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`,
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`,
);
try {
const { body, headers } = await http.get(url);
let { body, headers, statusCode } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
$.info(`statusCode: ${statusCode}`);
if (statusCode < 200 || statusCode >= 400) {
throw new Error(`statusCode: ${statusCode}`);
}
if (headers) {
const flowInfo = getFlowField(headers);
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
headersResourceCache.set(id, flowInfo);
}
}
if (body.replace(/\s/g, '').length === 0)
throw new Error(new Error('远程资源内容为空'));
if (preprocess) {
try {
if (clashPreprocessor.test(body)) {
body = clashPreprocessor.parse(body, true);
}
} catch (e) {
$.error(`Clash Pre-processor error: ${e}`);
}
}
let shouldCache = true;
if (cacheThreshold) {
const size = body.length / 1024;
if (size > cacheThreshold) {
$.info(
`资源大小 ${size.toFixed(
2,
)} KB 超过了 ${cacheThreshold} KB, 不缓存`,
);
shouldCache = false;
}
}
if (shouldCache) {
resourceCache.set(id, body);
if (customCacheKey) {
let shouldWriteCustomCacheKey = true;
if (preprocess) {
try {
const proxies = ProxyUtils.parse(body);
if (
!Array.isArray(proxies) ||
proxies.length === 0
) {
$.error(
`URL ${url} 不包含有效节点\n不写入自定义缓存 ${$arguments?.cacheKey}`,
);
shouldWriteCustomCacheKey = false;
}
} catch (e) {
$.error(
`URL ${url} 尝试解析节点失败 ${
e.message ?? e
}\n不写入自定义缓存 ${$arguments?.cacheKey}`,
);
shouldWriteCustomCacheKey = false;
}
}
if (shouldWriteCustomCacheKey) {
$.info(
`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`,
);
$.write(body, customCacheKey);
}
}
}
resourceCache.set(id, body);
result = body;
} catch (e) {
if (customCacheKey) {
const cached = $.read(customCacheKey);
if (cached) {
$.info(
`无法下载 URL ${url}: ${
e.message ?? e
}\n使用自定义缓存 ${$arguments?.cacheKey}`,
);
return cached;
}
}
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
}
}
@@ -101,7 +314,17 @@ export default async function download(rawUrl, ua, timeout) {
// 检查订阅有效性
if ($arguments?.validCheck) {
await validCheck(parseFlowHeaders(await getFlowHeaders(url)));
await validCheck(
parseFlowHeaders(
await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy,
$arguments.flowUrl,
),
),
);
}
if (!isNode) {
@@ -109,3 +332,25 @@ export default async function download(rawUrl, ua, timeout) {
}
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

@@ -1,7 +1,17 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
const {
isNode,
isQX,
isLoon,
isSurge,
isStash,
isShadowRocket,
isLanceX,
isEgern,
isGUIforCores,
} = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
@@ -9,8 +19,51 @@ if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
if (isEgern) backend = 'Egern';
if (isLanceX) backend = 'LanceX';
if (isGUIforCores) backend = 'GUI.for.Cores';
let meta = {};
let feature = {};
try {
if (typeof $environment !== 'undefined') {
// eslint-disable-next-line no-undef
meta.env = $environment;
}
if (typeof $loon !== 'undefined') {
// eslint-disable-next-line no-undef
meta.loon = $loon;
}
if (typeof $script !== 'undefined') {
// eslint-disable-next-line no-undef
meta.script = $script;
}
if (typeof $Plugin !== 'undefined') {
// eslint-disable-next-line no-undef
meta.plugin = $Plugin;
}
if (isNode) {
meta.node = {
version: eval('process.version'),
argv: eval('process.argv'),
filename: eval('__filename'),
dirname: eval('__dirname'),
env: {},
};
const env = eval('process.env');
for (const key in env) {
if (/^SUB_STORE_/.test(key)) {
meta.node.env[key] = env[key];
}
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
export default {
backend,
version: substoreVersion,
feature,
meta,
};

View File

@@ -1,16 +1,35 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
export function getFlowField(headers) {
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
const keys = Object.keys(headers);
let sub = '';
let webPage = '';
for (let k of keys) {
const lower = k.toLowerCase();
if (lower === 'subscription-userinfo') {
sub = headers[k];
} else if (lower === 'profile-web-page-url') {
webPage = headers[k];
}
}
return `${sub || ''}${
webPage ? `; app_url=${encodeURIComponent(webPage)}` : ''
}`;
}
export async function getFlowHeaders(rawUrl, ua, timeout) {
let url = rawUrl;
export async function getFlowHeaders(
rawUrl,
ua,
timeout,
customProxy,
flowUrl,
) {
let url = flowUrl || rawUrl || '';
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
@@ -30,56 +49,129 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
}
}
}
if ($arguments?.noFlow) {
if ($arguments?.noFlow || !/^https?/.test(url)) {
return;
}
const cached = headersResourceCache.get(url);
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const insecure = $arguments?.insecure
? $.env.isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultFlowUserAgent || 'clash';
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
const cached = headersResourceCache.get(id);
let flowInfo;
if (!$arguments?.noCache && cached) {
// $.info(`使用缓存的流量信息: ${url}`);
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
flowInfo = cached;
} else {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY);
const userAgent =
ua ||
defaultFlowUserAgent ||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
const requestTimeout = timeout || defaultTimeout;
const http = HTTP();
try {
// $.info(`使用 HEAD 方法获取流量信息: ${url}`);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法获取流量信息失败: ${url}: ${e.message ?? e}`,
if (flowUrl) {
$.info(
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
}
if (!flowInfo) {
$.info(`使用 GET 方法获取流量信息: ${url}`);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
const { body } = await http.get({
url: flowUrl,
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
flowInfo = body;
} else {
try {
$.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
e.message ?? e
}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
}
}
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
flowInfo = flowInfo.trim();
}
if (flowInfo) {
headersResourceCache.set(id, flowInfo);
}
}
@@ -110,13 +202,34 @@ export function parseFlowHeaders(flowHeaders) {
? Number(expireMatch[1] + expireMatch[2])
: undefined;
return { expires, total, usage: { upload, download } };
const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
const remainingDays = remainingDaysMatch
? Number(remainingDaysMatch[1])
: undefined;
const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
const planName = planNameMatch
? decodeURIComponent(planNameMatch[1])
: undefined;
return {
expires,
total,
usage: { upload, download },
remainingDays,
appUrl,
planName,
};
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024
return flow < 1024 || unitIndex === unitList.length - 1
? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]);
}
@@ -143,3 +256,113 @@ export function validCheck(flow) {
}
}
}
export function getRmainingDays(opt = {}) {
try {
let { resetDay, startDate, cycleDays } = opt;
if (['string', 'number'].includes(typeof opt)) {
resetDay = opt;
}
if (startDate && cycleDays) {
cycleDays = parseInt(cycleDays);
if (isNaN(cycleDays) || cycleDays <= 0)
throw new Error('重置周期应为正整数');
if (!startDate || !Date.parse(startDate))
throw new Error('开始日期不合法');
const start = new Date(startDate);
const today = new Date();
start.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
if (start.getTime() > today.getTime())
throw new Error('开始日期应早于现在');
let resetDate = new Date(startDate);
resetDate.setDate(resetDate.getDate() + cycleDays);
while (resetDate < today) {
resetDate.setDate(resetDate.getDate() + cycleDays);
}
resetDate.setHours(0, 0, 0, 0);
const timeDiff = resetDate.getTime() - today.getTime();
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
return daysDiff;
} else {
if (!resetDay) return;
resetDay = parseInt(resetDay);
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
throw new Error('月重置日应为 1-31 之间的整数');
let now = new Date();
let today = now.getDate();
let month = now.getMonth();
let year = now.getFullYear();
let daysInMonth;
if (resetDay > today) {
daysInMonth = 0;
} else {
daysInMonth = new Date(year, month + 1, 0).getDate();
}
return daysInMonth - today + resetDay;
}
} catch (e) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}
export function normalizeFlowHeader(flowHeaders) {
try {
// 使用 Map 保持顺序并处理重复键
const kvMap = new Map();
flowHeaders
.split(';')
.map((p) => p.trim())
.filter(Boolean)
.forEach((pair) => {
const eqIndex = pair.indexOf('=');
if (eqIndex === -1) return;
const key = pair.slice(0, eqIndex).trim();
const encodedValue = pair.slice(eqIndex + 1).trim();
// 只保留第一个出现的 key
if (!kvMap.has(key)) {
try {
// 解码 URI 组件并保留原始值作为 fallback
let decodedValue = decodeURIComponent(encodedValue);
if (
['upload', 'download', 'total', 'expire'].includes(
key,
)
) {
try {
decodedValue = Number(decodedValue).toFixed(0);
} catch (e) {
$.error(
`Failed to convert value for key "${key}=${encodedValue}": ${
e.message ?? e
}`,
);
}
}
kvMap.set(key, decodedValue);
} catch (e) {
kvMap.set(key, encodedValue);
}
}
});
// 拼接标准化字符串
return Array.from(kvMap.entries())
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
.join('; ');
} catch (e) {
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
return flowHeaders;
}
}

View File

@@ -1,3 +1,116 @@
import $ from '@/core/app';
const ISOFlags = {
'🏳️‍🌈': ['EXP', 'BAND'],
'🇸🇱': ['TEST', 'SOS'],
'🇦🇩': ['AD', 'AND'],
'🇦🇪': ['AE', 'ARE'],
'🇦🇫': ['AF', 'AFG'],
'🇦🇱': ['AL', 'ALB'],
'🇦🇲': ['AM', 'ARM'],
'🇦🇷': ['AR', 'ARG'],
'🇦🇹': ['AT', 'AUT'],
'🇦🇺': ['AU', 'AUS'],
'🇦🇿': ['AZ', 'AZE'],
'🇧🇦': ['BA', 'BIH'],
'🇧🇩': ['BD', 'BGD'],
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇴': ['BO', 'BOL'],
'🇧🇳': ['BN', 'BRN'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇹': ['BT', 'BTN'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
'🇨🇱': ['CL', 'CHL'],
'🇨🇴': ['CO', 'COL'],
'🇨🇷': ['CR', 'CRI'],
'🇨🇾': ['CY', 'CYP'],
'🇨🇿': ['CZ', 'CZE'],
'🇩🇪': ['DE', 'DEU'],
'🇩🇰': ['DK', 'DNK'],
'🇪🇨': ['EC', 'ECU'],
'🇪🇪': ['EE', 'EST'],
'🇪🇬': ['EG', 'EGY'],
'🇪🇸': ['ES', 'ESP'],
'🇪🇺': ['EU'],
'🇫🇮': ['FI', 'FIN'],
'🇫🇷': ['FR', 'FRA'],
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇬🇹': ['GT', 'GTM'],
'🇬🇺': ['GU', 'GUM'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN', 'TYO'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],
'🇰🇵': ['KP', 'PRK'],
'🇰🇷': ['KR', 'KOR', 'SEL'],
'🇰🇿': ['KZ', 'KAZ'],
'🇮🇩': ['ID', 'IDN'],
'🇮🇪': ['IE', 'IRL'],
'🇮🇱': ['IL', 'ISR'],
'🇮🇲': ['IM', 'IMN'],
'🇮🇳': ['IN', 'IND'],
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇦': ['LA', 'LAO'],
'🇱🇰': ['LK', 'LKA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇲': ['MM', 'MMR'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
'🇲🇹': ['MT', 'MLT'],
'🇲🇽': ['MX', 'MEX'],
'🇲🇾': ['MY', 'MYS'],
'🇳🇱': ['NL', 'NLD', 'AMS'],
'🇳🇴': ['NO', 'NOR'],
'🇳🇵': ['NP', 'NPL'],
'🇳🇿': ['NZ', 'NZL'],
'🇵🇦': ['PA', 'PAN'],
'🇵🇪': ['PE', 'PER'],
'🇵🇭': ['PH', 'PHL'],
'🇵🇰': ['PK', 'PAK'],
'🇵🇱': ['PL', 'POL'],
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇵🇬': ['PG', 'PNG'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
'🇷🇺': ['RU', 'RUS'],
'🇸🇦': ['SA', 'SAU'],
'🇸🇪': ['SE', 'SWE'],
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO', 'SJC'],
'🇺🇾': ['UY', 'URY'],
'🇻🇪': ['VE', 'VEN'],
'🇻🇳': ['VN', 'VNM'],
'🇿🇦': ['ZA', 'ZAF', 'JNB'],
'🇨🇳': ['CN', 'CHN', 'BACK'],
};
// get proxy flag according to its name
export function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
@@ -36,7 +149,10 @@ export function getFlag(name) {
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
'🇧🇭': ['Bahrain', '巴林'],
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
'🇧🇳': ['Brunei', '文莱', '汶萊'],
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
'🇧🇴': ['Bolivia', '玻利维亚'],
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
'🇨🇦': [
'Canada',
'加拿大',
@@ -47,6 +163,7 @@ export function getFlag(name) {
'滑铁卢',
'多伦多',
'Waterloo',
'Toronto',
],
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
'🇨🇱': ['Chile', '智利'],
@@ -65,6 +182,7 @@ export function getFlag(name) {
'广德',
'法兰克福',
'Frankfurt',
'德意志',
],
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
'🇪🇨': ['Ecuador', '厄瓜多尔'],
@@ -85,6 +203,8 @@ export function getFlag(name) {
],
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
'🇬🇷': ['Greece', '希腊', '希臘'],
'🇬🇺': ['Guam', '关岛', '關島'],
'🇬🇹': ['Guatemala', '危地马拉'],
'🇭🇰': [
'Hongkong',
'香港',
@@ -140,15 +260,18 @@ export function getFlag(name) {
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
'🇮🇱': ['Israel', '以色列'],
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
'🇮🇳': ['India', '印度', '孟买', 'MFumbai'],
'🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],
'🇮🇷': ['Iran', '伊朗'],
'🇮🇸': ['Iceland', '冰岛', '冰島'],
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
'🇱🇦': ['Laos', '老挝', '老撾'],
'🇱🇹': ['Lithuania', '立陶宛'],
'🇱🇺': ['Luxembourg', '卢森堡'],
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
'🇲🇦': ['Morocco', '摩洛哥'],
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
'🇲🇳': ['Mongolia', '蒙古'],
@@ -156,7 +279,14 @@ export function getFlag(name) {
'🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇱': [
'Netherlands',
'荷兰',
'荷蘭',
'尼德蘭',
'阿姆斯特丹',
'Amsterdam',
],
'🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'],
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
@@ -164,9 +294,10 @@ export function getFlag(name) {
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
'🇵🇰': ['Pakistan', '巴基斯坦'],
'🇵🇱': ['Poland', '波兰', '波蘭'],
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
'🇵🇷': ['Puerto Rico', '波多黎各'],
'🇵🇹': ['Portugal', '葡萄牙'],
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
'🇵🇾': ['Paraguay', '巴拉圭'],
'🇷🇴': ['Romania', '罗马尼亚'],
'🇷🇸': ['Serbia', '塞尔维亚'],
@@ -188,8 +319,8 @@ export function getFlag(name) {
'沪俄',
'Moscow',
],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'],
'🇸🇪': ['Sweden', '瑞典'],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
'🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],
'🇸🇬': [
'Singapore',
'新加坡',
@@ -209,7 +340,7 @@ export function getFlag(name) {
'🇸🇰': ['Slovakia', '斯洛伐克'],
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
'🇹🇳': ['Tunisia', '突尼斯'],
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔'],
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],
'🇹🇼': [
'Taiwan',
'台湾',
@@ -224,6 +355,7 @@ export function getFlag(name) {
'台',
'臺',
'Taipei',
'Tai Wan',
],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
'🇺🇸': [
@@ -235,6 +367,7 @@ export function getFlag(name) {
'波特兰',
'达拉斯',
'俄勒冈',
'Oregon',
'凤凰城',
'费利蒙',
'硅谷',
@@ -248,10 +381,17 @@ export function getFlag(name) {
'沪美',
'哥伦布',
'纽约',
'New York',
'Los Angeles',
'San Jose',
'Sillicon Valley',
'Michigan',
'俄亥俄',
'Ohio',
'马纳萨斯',
'Manassas',
'弗吉尼亚',
'Virginia',
],
'🇺🇾': ['Uruguay', '乌拉圭'],
'🇻🇪': ['Venezuela', '委内瑞拉'],
@@ -283,108 +423,6 @@ export function getFlag(name) {
],
};
const ISOFlags = {
'🏳️‍🌈': ['EXP', 'BAND'],
'🇸🇱': ['TEST', 'SOS'],
'🇦🇩': ['AD', 'AND'],
'🇦🇪': ['AE', 'ARE'],
'🇦🇫': ['AF', 'AFG'],
'🇦🇱': ['AL', 'ALB'],
'🇦🇲': ['AM', 'ARM'],
'🇦🇷': ['AR', 'ARG'],
'🇦🇹': ['AT', 'AUT'],
'🇦🇺': ['AU', 'AUS'],
'🇦🇿': ['AZ', 'AZE'],
'🇧🇦': ['BA', 'BIH'],
'🇧🇩': ['BD', 'BGD'],
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
'🇨🇱': ['CL', 'CHL'],
'🇨🇴': ['CO', 'COL'],
'🇨🇷': ['CR', 'CRI'],
'🇨🇾': ['CY', 'CYP'],
'🇨🇿': ['CZ', 'CZE'],
'🇩🇪': ['DE', 'DEU'],
'🇩🇰': ['DK', 'DNK'],
'🇪🇨': ['EC', 'ECU'],
'🇪🇪': ['EE', 'EST'],
'🇪🇬': ['EG', 'EGY'],
'🇪🇸': ['ES', 'ESP'],
'🇪🇺': ['EU'],
'🇫🇮': ['FI', 'FIN'],
'🇫🇷': ['FR', 'FRA'],
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN'],
'🇰🇪': ['KE', 'KEN'],
'🇰🇬': ['KG', 'KGZ'],
'🇰🇭': ['KH', 'KGZ'],
'🇰🇵': ['KP', 'PRK'],
'🇰🇷': ['KR', 'KOR'],
'🇰🇿': ['KZ', 'KAZ'],
'🇮🇩': ['ID', 'IDN'],
'🇮🇪': ['IE', 'IRL'],
'🇮🇱': ['IL', 'ISR'],
'🇮🇲': ['IM', 'IMN'],
'🇮🇳': ['IN', 'IND'],
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
'🇲🇹': ['MT', 'MLT'],
'🇲🇽': ['MX', 'MEX'],
'🇲🇾': ['MY', 'MYS'],
'🇳🇱': ['NL', 'NLD'],
'🇳🇴': ['NO', 'NOR'],
'🇳🇵': ['NP', 'NPL'],
'🇳🇿': ['NZ', 'NZL'],
'🇵🇦': ['PA', 'PAN'],
'🇵🇪': ['PE', 'PER'],
'🇵🇭': ['PH', 'PHL'],
'🇵🇰': ['PK', 'PAK'],
'🇵🇱': ['PL', 'POL'],
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
'🇷🇺': ['RU', 'RUS'],
'🇸🇦': ['SA', 'SAU'],
'🇸🇪': ['SE', 'SWE'],
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
'🇺🇾': ['UY', 'URY'],
'🇻🇪': ['VE', 'VEN'],
'🇻🇳': ['VN', 'VNM'],
'🇿🇦': ['ZA', 'ZAF'],
'🇨🇳': ['CN', 'CHN', 'BACK'],
};
// 原旗帜或空
let Flag =
name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/)?.[0] ||
@@ -398,7 +436,9 @@ export function getFlag(name) {
// 不精确匹配(只要包含就算,忽略大小写)
keywords.some((keyword) => RegExp(`${keyword}`, 'i').test(name))
) {
//console.log(`newFlag = ${flag}`)
if (/内蒙古/.test(name) && ['🇲🇳'].includes(flag)) {
return (Flag = '🇨🇳');
}
return (Flag = flag);
}
}
@@ -412,10 +452,67 @@ export function getFlag(name) {
RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
)
) {
//console.log(`ISOFlag = ${flag}`)
return (Flag = flag);
const isCN2 =
flag == '🇨🇳' &&
RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);
if (!isCN2) {
return (Flag = flag);
}
}
}
//console.log(`Final Flag = ${Flag}`)
return Flag;
}
export function getISO(name) {
return ISOFlags[getFlag(name)]?.[0];
}
// remove flag
export function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')
.trim();
}
export class MMDB {
constructor({ country, asn } = {}) {
if ($.env.isNode) {
const Reader = eval(`require("@maxmind/geoip2-node")`).Reader;
const fs = eval("require('fs')");
const countryFile =
country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH');
// $.info(
// `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync(
// countryFile,
// )}`,
// );
if (countryFile) {
this.countryReader = Reader.openBuffer(
fs.readFileSync(countryFile),
);
}
// $.info(
// `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync(
// asnFile,
// )}`,
// );
if (asnFile) {
if (!fs.existsSync(asnFile))
throw new Error('GeoLite2 ASN MMDB does not exist');
this.asnReader = Reader.openBuffer(fs.readFileSync(asnFile));
}
}
}
geoip(ip) {
return this.countryReader?.country(ip)?.country?.isoCode;
}
ipaso(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemOrganization;
}
ipasn(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemNumber;
}
}

View File

@@ -1,10 +1,21 @@
import { HTTP } from '@/vendor/open-api';
import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import { SETTINGS_KEY } from '@/constants';
/**
* Gist backup
*/
export default class Gist {
constructor({ token, key, syncPlatform }) {
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultProxy, defaultTimeout: timeout } = $.read(SETTINGS_KEY);
let proxy = defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
if (syncPlatform === 'gitlab') {
this.headers = {
'PRIVATE-TOKEN': `${token}`,
@@ -13,7 +24,25 @@ export default class Gist {
};
this.http = HTTP({
baseURL: 'https://gitlab.com/api/v4',
headers: { ...this.headers },
headers: {
...this.headers,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout: timeout || 8000,
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
@@ -35,7 +64,25 @@ export default class Gist {
};
this.http = HTTP({
baseURL: 'https://api.github.com',
headers: { ...this.headers },
headers: {
...this.headers,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout: timeout || 8000,
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
@@ -67,15 +114,18 @@ export default class Gist {
return;
});
} else {
return this.http.get('/gists').then((response) => {
const gists = JSON.parse(response.body);
for (let g of gists) {
if (g.description === this.key) {
return g;
return this.http
.get('/gists?per_page=100&page=1')
.then((response) => {
const gists = JSON.parse(response.body);
$.info(`获取到当前 GitHub 用户的 gist: ${gists.length}`);
for (let g of gists) {
if (g.description === this.key) {
return g;
}
}
}
return;
});
return;
});
}
}
@@ -135,9 +185,9 @@ export default class Gist {
}
}
});
console.log(`result`, result);
console.log(`files`, files);
console.log(`actions`, actions);
// console.log(`result`, result);
// console.log(`files`, files);
// console.log(`actions`, actions);
if (this.syncPlatform === 'gitlab') {
if (Object.keys(result).length === 0) {
@@ -230,7 +280,7 @@ export default class Gist {
return Promise.reject(err);
}
} else {
return Promise.reject('找不到 Sub-Store Gist');
return Promise.reject(`找不到 Sub-Store Gist (${this.key})`);
}
}
}

View File

@@ -1,3 +1,4 @@
import * as ipAddress from 'ip-address';
// source: https://stackoverflow.com/a/36760050
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
@@ -35,7 +36,124 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue;
}
function getPolicyDescriptor(str) {
if (!str) return {};
return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str)
? {
'policy-descriptor': str,
}
: {
policy: str,
};
}
// const utf8ArrayToStr =
// typeof TextDecoder !== 'undefined'
// ? (v) => new TextDecoder().decode(new Uint8Array(v))
// : (function () {
// var charCache = new Array(128); // Preallocate the cache for the common single byte chars
// var charFromCodePt = String.fromCodePoint || String.fromCharCode;
// var result = [];
// return function (array) {
// var codePt, byte1;
// var buffLen = array.length;
// result.length = 0;
// for (var i = 0; i < buffLen; ) {
// byte1 = array[i++];
// if (byte1 <= 0x7f) {
// codePt = byte1;
// } else if (byte1 <= 0xdf) {
// codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f);
// } else if (byte1 <= 0xef) {
// codePt =
// ((byte1 & 0x0f) << 12) |
// ((array[i++] & 0x3f) << 6) |
// (array[i++] & 0x3f);
// } else if (String.fromCodePoint) {
// codePt =
// ((byte1 & 0x07) << 18) |
// ((array[i++] & 0x3f) << 12) |
// ((array[i++] & 0x3f) << 6) |
// (array[i++] & 0x3f);
// } else {
// codePt = 63; // Cannot convert four byte code points, so use "?" instead
// i += 3;
// }
// result.push(
// charCache[codePt] ||
// (charCache[codePt] = charFromCodePt(codePt)),
// );
// }
// return result.join('');
// };
// })();
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomPort(portString) {
let portParts = portString.split(/,|\//);
let randomPart = portParts[Math.floor(Math.random() * portParts.length)];
if (randomPart.includes('-')) {
let [min, max] = randomPart.split('-').map(Number);
return getRandomInt(min, max);
} else {
return Number(randomPart);
}
}
function numberToString(value) {
return Number.isSafeInteger(value)
? String(value)
: BigInt(value).toString();
}
function isValidUUID(uuid) {
return (
typeof uuid === 'string' &&
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
uuid,
)
);
}
function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) {
return '';
}
const pad = (num) => String(num).padStart(2, '0');
const replacements = {
YYYY: d.getFullYear(),
MM: pad(d.getMonth() + 1),
DD: pad(d.getDate()),
HH: pad(d.getHours()),
mm: pad(d.getMinutes()),
ss: pad(d.getSeconds()),
};
return format.replace(
/YYYY|MM|DD|HH|mm|ss/g,
(match) => replacements[match],
);
}
export {
formatDateTime,
isValidUUID,
ipAddress,
isIPv4,
isIPv6,
isValidPortNumber,
@@ -43,4 +161,8 @@ export {
getIfNotBlank,
isPresent,
getIfPresent,
// utf8ArrayToStr,
getPolicyDescriptor,
getRandomPort,
numberToString,
};

View File

@@ -4,6 +4,8 @@ import {
SCHEMA_VERSION_KEY,
ARTIFACTS_KEY,
RULES_KEY,
FILES_KEY,
TOKENS_KEY,
} from '@/constants';
import $ from '@/core/app';
@@ -55,7 +57,17 @@ function doMigrationV2() {
const newRules = Object.values(rules);
$.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;
$.info('Migration complete!');

View File

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

11
backend/src/utils/rs.js Normal file
View File

@@ -0,0 +1,11 @@
import rs from 'jsrsasign';
export function generateFingerprint(caStr) {
const hex = rs.pemtohex(caStr);
const fingerPrint = rs.KJUR.crypto.Util.hashHex(hex, 'sha256');
return fingerPrint.match(/.{2}/g).join(':').toUpperCase();
}
export default {
generateFingerprint,
};

View File

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

View File

@@ -0,0 +1,101 @@
import gte from 'semver/functions/gte';
import coerce from 'semver/functions/coerce';
import $ from '@/core/app';
export function getUserAgentFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
let accept = '';
for (let k of keys) {
const lower = k.toLowerCase();
if (lower === 'user-agent') {
UA = headers[k];
ua = UA.toLowerCase();
} else if (lower === 'accept') {
accept = headers[k];
}
}
return { UA, ua, accept };
}
export function getPlatformFromUserAgent({ ua, UA, accept }) {
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (ua.indexOf('egern') !== -1) {
return 'Egern';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'Shadowrocket';
} else if (UA.indexOf('Stash') !== -1) {
return 'Stash';
} else if (
ua === 'meta' ||
(ua.indexOf('clash') !== -1 && ua.indexOf('meta') !== -1) ||
ua.indexOf('clash-verge') !== -1 ||
ua.indexOf('flclash') !== -1
) {
return 'ClashMeta';
} else if (ua.indexOf('clash') !== -1) {
return 'Clash';
} else if (ua.indexOf('v2ray') !== -1) {
return 'V2Ray';
} else if (ua.indexOf('sing-box') !== -1 || ua.indexOf('singbox') !== -1) {
return 'sing-box';
} else if (accept.indexOf('application/json') === 0) {
return 'JSON';
} else {
return 'V2Ray';
}
}
export function getPlatformFromHeaders(headers) {
const { UA, ua, accept } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA, accept });
}
export function shouldIncludeUnsupportedProxy(platform, ua) {
// try {
// const target = getPlatformFromUserAgent({
// UA: ua,
// ua: ua.toLowerCase(),
// });
// if (!['Stash', 'Egern', 'Loon'].includes(target)) {
// return false;
// }
// const coerceVersion = coerce(ua);
// $.log(JSON.stringify(coerceVersion, null, 2));
// const { version } = coerceVersion;
// if (
// platform === 'Stash' &&
// target === 'Stash' &&
// gte(version, '3.1.0')
// ) {
// return true;
// }
// if (
// platform === 'Egern' &&
// target === 'Egern' &&
// gte(version, '1.29.0')
// ) {
// return true;
// }
// // Loon 的 UA 不规范, version 取出来是 build
// if (
// platform === 'Loon' &&
// target === 'Loon' &&
// gte(version, '842.0.0')
// ) {
// return true;
// }
// } catch (e) {
// $.error(`获取版本号失败: ${e}`);
// }
return false;
}

View File

@@ -17,16 +17,16 @@ function retry(fn, content, ...args) {
}
export function safeLoad(content, ...args) {
return retry(YAML.safeLoad, content, ...args);
return retry(YAML.safeLoad, JSON.parse(JSON.stringify(content)), ...args);
}
export function load(content, ...args) {
return retry(YAML.load, content, ...args);
return retry(YAML.load, JSON.parse(JSON.stringify(content)), ...args);
}
export function safeDump(...args) {
return YAML.safeDump(...args);
export function safeDump(content, ...args) {
return YAML.safeDump(JSON.parse(JSON.stringify(content)), ...args);
}
export function dump(...args) {
return YAML.dump(...args);
export function dump(content, ...args) {
return YAML.dump(JSON.parse(JSON.stringify(content)), ...args);
}
export default {
@@ -34,4 +34,6 @@ export default {
load,
safeDump,
dump,
parse: safeLoad,
stringify: safeDump,
};

View File

@@ -9,6 +9,7 @@ export default function express({ substore: $, port, host }) {
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept',
'X-Powered-By': 'Sub-Store',
};
// node support
@@ -16,7 +17,14 @@ export default function express({ substore: $, port, host }) {
const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`);
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(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
);
@@ -30,7 +38,7 @@ export default function express({ substore: $, port, host }) {
app.start = () => {
const listener = app.listen(port, host, () => {
const { address, port } = listener.address();
$.info(`[BACKEND] ${address}:${port}`);
$.info(`[BACKEND] listening on ${address}:${port}`);
});
};
return app;
@@ -161,7 +169,7 @@ export default function express({ substore: $, port, host }) {
function Response() {
let statusCode = 200;
const { isQX, isLoon, isSurge } = ENV();
const { isQX, isLoon, isSurge, isGUIforCores } = ENV();
const headers = DEFAULT_HEADERS;
const STATUS_CODE_MAP = {
200: 'HTTP/1.1 200 OK',
@@ -184,7 +192,7 @@ export default function express({ substore: $, port, host }) {
body,
headers,
};
if (isQX) {
if (isQX || isGUIforCores) {
$done(response);
} else if (isLoon || isSurge) {
$done({

View File

@@ -6,7 +6,39 @@ const isNode = eval(`typeof process !== "undefined"`); // eval is needed in orde
const isStash =
'undefined' !== typeof $environment && $environment['stash-version'];
const isShadowRocket = 'undefined' !== typeof $rocket;
const isEgern = 'object' == typeof egern;
const isLanceX = 'undefined' != typeof $native;
const isGUIforCores = typeof $Plugins !== 'undefined';
import { Base64 } from 'js-base64';
function isPlainObject(obj) {
return (
obj !== null &&
typeof obj === 'object' &&
[null, Object.prototype].includes(Object.getPrototypeOf(obj))
);
}
function parseSocks5Uri(uri) {
// eslint-disable-next-line no-unused-vars
let [__, username, password, server, port, query, name] = uri.match(
/^socks5:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${uri}`);
throw new Error(`port is not present in line: ${uri}`);
}
return {
type: 5,
host: server,
port,
userId: username != null ? decodeURIComponent(username) : undefined,
password: password != null ? decodeURIComponent(password) : undefined,
};
}
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
this.name = name;
@@ -15,6 +47,10 @@ export class OpenAPI {
this.http = HTTP();
this.env = ENV();
if (isNode) {
const dotenv = eval(`require("dotenv")`);
dotenv.config();
}
this.node = (() => {
if (isNode) {
const fs = eval("require('fs')");
@@ -46,35 +82,69 @@ export class OpenAPI {
this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
if (isLoon || isSurge)
this.cache = JSON.parse($persistentStore.read(this.name) || '{}');
if (isGUIforCores)
this.cache = JSON.parse(
$Plugins.SubStoreCache.get(this.name) || '{}',
);
if (isNode) {
// create a json for root cache
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
let rootPath = `${basePath}/root.json`;
const backupRootPath = `${basePath}/root_${Date.now()}.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
if (this.node.fs.existsSync(rootPath)) {
try {
this.root = JSON.parse(
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: 'wx',
flag: 'w',
});
this.root = {};
} else {
this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
}
// create a json file with the given name if not exists
let fpath = `${basePath}/${this.name}.json`;
const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
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}`, 'utf-8'),
);
} catch (e) {
try {
const str = Base64.decode(
this.node.fs.readFileSync(`${fpath}`, 'utf-8'),
);
this.cache = JSON.parse(str);
this.node.fs.writeFileSync(fpath, str, {
flag: 'w',
});
} 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({}), {
flag: 'wx',
flag: 'w',
});
this.cache = {};
} else {
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
}
}
}
@@ -84,6 +154,7 @@ export class OpenAPI {
const data = JSON.stringify(this.cache, null, 2);
if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isGUIforCores) $Plugins.SubStoreCache.set(this.name, data);
if (isNode) {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
@@ -116,6 +187,9 @@ export class OpenAPI {
if (isNode) {
this.root[key] = data;
}
if (isGUIforCores) {
return $Plugins.SubStoreCache.set(key, data);
}
} else {
this.cache[key] = data;
}
@@ -135,6 +209,9 @@ export class OpenAPI {
if (isNode) {
return this.root[key];
}
if (isGUIforCores) {
return $Plugins.SubStoreCache.get(key);
}
} else {
return this.cache[key];
}
@@ -153,6 +230,9 @@ export class OpenAPI {
if (isNode) {
delete this.root[key];
}
if (isGUIforCores) {
return $Plugins.SubStoreCache.remove(key);
}
} else {
delete this.cache[key];
}
@@ -218,6 +298,9 @@ export class OpenAPI {
});
}
}
if (isGUIforCores) {
$Plugins.Notify(title, subtitle + '\n' + content);
}
}
// other helper functions
@@ -238,7 +321,7 @@ export class OpenAPI {
}
done(value = {}) {
if (isQX || isLoon || isSurge) {
if (isQX || isLoon || isSurge || isGUIforCores) {
$done(value);
} else if (isNode) {
if (typeof $context !== 'undefined') {
@@ -251,11 +334,21 @@ export class OpenAPI {
}
export function ENV() {
return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket };
return {
isQX,
isLoon,
isSurge,
isNode,
isStash,
isShadowRocket,
isEgern,
isLanceX,
isGUIforCores,
};
}
export function HTTP(defaultOptions = { baseURL: '' }) {
const { isQX, isLoon, isSurge, isNode } = ENV();
const { isQX, isLoon, isSurge, isNode, isGUIforCores } = ENV();
const methods = [
'GET',
'POST',
@@ -305,42 +398,166 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
url: options.url,
headers: options.headers,
body: options.body,
opts: options.opts,
});
} else if (isLoon || isSurge || isNode) {
worker = new Promise((resolve, reject) => {
const request = isNode
? eval("require('request')")
: $httpClient;
request[method.toLowerCase()](
JSON.parse(JSON.stringify(options)),
(err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
worker = new Promise(async (resolve, reject) => {
const body = options.body;
const opts = JSON.parse(JSON.stringify(options));
opts.body = body;
opts.timeout = opts.timeout || 8000;
if (opts.timeout) {
opts.timeout++;
if (isNaN(opts.timeout)) {
opts.timeout = 8000;
}
if (!isNode) {
let unit = 'ms';
// 这些客户端单位为 s
if (isSurge || isStash || isShadowRocket) {
opts.timeout = Math.ceil(opts.timeout / 1000);
unit = 's';
}
// Loon 为 ms
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
}
}
if (isNode) {
const undici = eval("require('undici')");
const { socksDispatcher } = eval("require('fetch-socks')");
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')}`,
};
}
let dispatcher;
if (!opts.proxy) {
const allProxy =
eval('process.env.all_proxy') ||
eval('process.env.ALL_PROXY');
if (allProxy && /^socks5:\/\//.test(allProxy)) {
opts.proxy = allProxy;
}
}
if (opts.proxy) {
if (/^socks5:\/\//.test(opts.proxy)) {
dispatcher = socksDispatcher(
parseSocks5Uri(opts.proxy),
agentOpts,
);
} else {
dispatcher = new ProxyAgent({
...agentOpts,
uri: opts.proxy,
});
}
} else {
dispatcher = new EnvHttpProxyAgent(agentOpts);
}
const response = await request(opts.url, {
...opts,
method: method.toUpperCase(),
dispatcher: dispatcher.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) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
if (err) reject(err);
else
resolve({
statusCode:
response.status || response.statusCode,
headers: response.headers,
body,
});
},
);
if (err) reject(err);
else
resolve({
statusCode:
response.status || response.statusCode,
headers: response.headers,
body,
});
},
);
}
});
} else if (isGUIforCores) {
worker = new Promise(async (resolve, reject) => {
try {
const response = await $Plugins.Requests({
method,
url: options.url,
headers: options.headers,
body: options.body,
autoTransformBody: false,
options: {
Proxy: options.proxy,
Timeout: options.timeout
? options.timeout / 1000
: 15,
},
});
resolve({
statusCode: response.status,
headers: response.headers,
body: response.body,
});
} catch (error) {
reject(error);
}
});
}
let timeoutid;
const timer = timeout
? new Promise((_, reject) => {
// console.log(`[request timeout] ${timeout}ms`);
timeoutid = setTimeout(() => {
events.onTimeout();
return reject(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

38
config/Egern.yaml Normal file
View File

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

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
#!openUrl=https://sub.store
#!author=Peng-YM
#!homepage=https://github.com/sub-store-org/Sub-Store
@@ -14,7 +14,7 @@ DOMAIN,sub-store.vercel.app,PROXY
hostname=sub.store
[Script]
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
cron "55 23 * * *" script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync

View File

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

View File

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

View File

@@ -6,22 +6,32 @@ Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](http
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置:
## 服务器/云平台/Docker/Android 版
https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87
## App 版
### 1. Loon
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的分类和参数设置): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
#### 关于 Surge 的格外说明
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)
> 最新 Surge iOS TestFlight 版本应该没有内存问题了 可以大胆尝试带 ability 参数版本
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
> 最新版 Surge 已删除 `ability: http-client-policy` 参数, 模块暂不做修改, 对测落地功能无影响
2. 经典版, 不支持编辑参数, 固定带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
3. 经典版, 不支持编辑参数, 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX
@@ -33,9 +43,20 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule) 即可。
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule) 即可。
### 6. Egern
安装使用 模块 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Egern.yaml) 即可。
## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
## 链接参数说明
https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
## 脚本使用说明
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E

View File

@@ -1,5 +1,5 @@
name: Sub-Store
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分
desc: 高级订阅管理工具 @Peng-YM. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
icon: https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png
http:
@@ -25,13 +25,13 @@ cron:
script-providers:
sub-store-0:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
interval: 86400
sub-store-1:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
interval: 86400
cron-sync-artifacts:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
interval: 86400

View File

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

View File

@@ -1,5 +1,5 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
#!category=订阅管理
[MITM]
@@ -7,7 +7,7 @@ hostname = %APPEND% sub.store
[Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js

View File

@@ -1,12 +1,12 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分
#!desc=高级订阅管理工具 @Peng-YM 带 ability 参数版本, 使用 jsc 引擎时, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
#!category=订阅管理
[MITM]
hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js

View File

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

View File

@@ -1,40 +0,0 @@
upstream api {
server 0.0.0.0:3000;
}
server {
listen 6080;
# allow 127.0.0.1;
# allow 0.0.0.0;
# deny all;
gzip on;
gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.0;
location / {
root /Sub-Store/web/dist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api;
}
location /download {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://api;
}
}

View File

@@ -1,27 +1,115 @@
function operator(proxies = [], targetPlatform, context) {
// 支持快捷操作 不一定要写一个 function
// 可参考 https://t.me/zhetengsha/970
// https://t.me/zhetengsha/1009
// https://t.me/zhetengsha/1009
// proxies 为传入的内部节点数组
// 结构大致参考了 Clash.Meta(mihomo) 有私货
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 1. `no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `resolved` 字段
// 3. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// 0. 结构大致参考了 Clash.Meta(mihomo), 可参考 mihomo 的文档, 例如 `xudp`, `smux` 都可以自己设置. 但是有私货, 下面是我能想起来的一些私货
// 1. `_no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
// 7. `tls-fingerprint` 为 tls 指纹
// 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
// 例如 $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 类协议的通用逻辑), 输出时删除
// 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本,兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server`
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
// 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件
// const fs = eval(`require("fs")`)
// // const path = eval(`require("path")`)
// fs.writeFileSync('/tmp/1.txt', $content, "utf8");
// $arguments 为传入的脚本参数
// $options 为通过链接传入的参数
// 例如: { arg1: 'a', arg2: 'b' }
// 可这样传:
// 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))
// /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D
// 或这样传:
// 先这样处理 encodeURIComponent('arg1=a&arg2=b')
// /api/file/foo?$options=arg1%3Da%26arg2%3Db
// 默认会带上 _req 字段, 结构为
// {
// method,
// url,
// path,
// query,
// params,
// headers,
// body,
// }
// console.log($options)
// 若设置 $options._res.headers
// 则会在输出文件时设置响应头, 例如:
// $options._res = {
// headers: {
// 'X-Custom': '1'
// }
// }
// targetPlatform 为输出的目标平台
// lodash
// $substore 为 OpenAPI
// 参考 https://github.com/Peng-YM/QuanX/blob/master/Tools/OpenAPI/README.md
// scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache
// 设置
// cache.set('a:1', 1)
// cache.set('a:2', 2)
// 获取
// cache.get('a:1')
// 支持第二个参数: 自定义过期时间
// 支持第三个参数: 是否删除过期项
// cache.get('a:2', 1000, true)
// 清理
// cache._cleanup()
// 支持第一个参数: 匹配前缀的项也一起删除
// 支持第二个参数: 自定义过期时间
// cache._cleanup('a:', 1000)
// 关于缓存时长
// 拉取 Sub-Store 订阅时, 会自动拉取远程订阅
// 远程订阅缓存是 1 小时, 缓存的唯一 key 为 url+ user agent. 可通过前端的刷新按钮刷新缓存. 或使用参数 noCache 来禁用缓存. 例: 内部配置订阅链接时使用 http://a.com#noCache, 外部使用 sub-store 链接时使用 https://sub.store/download/1?noCache=true
// 当使用相关脚本时, 若在对应的脚本中使用参数开启缓存, 可设置持久化缓存 sub-store-csr-expiration-time 的值来自定义默认缓存时长, 默认为 172800000 (48 * 3600 * 1000, 即 48 小时)
// 🎈Loon 可在插件中设置
// 其他平台同理, 持久化缓存数据在 JSON 里
// 当配合脚本使用时, 可以在脚本的前面添加一个脚本操作, 实现保留 1 小时的缓存. 这样比较灵活
// async function operator() {
// scriptResourceCache._cleanup(undefined, 1 * 3600 * 1000);
// }
// ProxyUtils 为节点处理工具
// 可参考 https://t.me/zhetengsha/1066
@@ -29,11 +117,51 @@ function operator(proxies = [], targetPlatform, context) {
// parse, // 订阅解析
// process, // 节点操作/文件操作
// produce, // 输出订阅
// getRandomPort, // 获取随机端口(参考 ports 端口跳跃的格式 443,8443,5000-6000)
// ipAddress, // https://github.com/beaugunderson/ip-address
// isIPv4,
// isIPv6,
// isIP,
// yaml, // yaml 解析和生成
// getFlag, // 获取 emoji 旗帜
// removeFlag, // 移除 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// downloadFile, // 下载二进制文件, 见 backend/src/utils/download.js
// 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
// JSON5, // https://github.com/json5/json5
// }
// 为兼容 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
// ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一
// 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 给节点名添加旗帜
// $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`
// 示例: 从 sni 文件中读取内容并进行节点操作
// const sni = await produceArtifact({
// type: 'file',
// name: 'sni' // 文件名
// });
// $server.sni = sni
// 示例: 从 config 文件中读取配置项并进行节点操作
// config 的本地内容为
// {
// "reuse": false
// }
// 脚本操作为
// const config = (ProxyUtils.JSON5 || JSON).parse(await produceArtifact({
// type: 'file',
// name: 'config' // 文件名
// }))
// $server.reuse = config.reuse
// 1. Surge 输出 WireGuard 完整配置
@@ -46,10 +174,13 @@ function operator(proxies = [], targetPlatform, context) {
// }
// })
// $content = proxies
// 2. sing-box
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
// 但是一般不需要这样用, 可参考
// 1. https://t.me/zhetengsha/1111
// 2. https://t.me/zhetengsha/1070
// 3. https://t.me/zhetengsha/1241
// let singboxProxies = await produceArtifact({
// type: 'subscription', // type: 'subscription' 或 'collection'
@@ -63,27 +194,49 @@ function operator(proxies = [], targetPlatform, context) {
// 3. clash.meta
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070
// 但是一般不需要这样用, 可参考
// 1. https://t.me/zhetengsha/1111
// 2. https://t.me/zhetengsha/1070
// 3. https://t.me/zhetengsha/1234
// let clashMetaProxies = await produceArtifact({
// type: 'subscription',
// name: 'sub',
// platform: 'ClashMeta',
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
// }))
// })
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
// 见 https://t.me/zhetengsha/1428
//
// const content = ProxyUtils.produce([...proxies], platform)
// // YAML
// ProxyUtils.yaml.load('YAML String')
// ProxyUtils.yaml.safeLoad('YAML String')
// $content = ProxyUtils.yaml.safeDump({})
// $content = ProxyUtils.yaml.dump({})
// 一个往文件里插入本地节点的例子:
// const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0])
// let clashMetaProxies = await produceArtifact({
// type: 'collection',
// name: '机场',
// platform: 'ClashMeta',
// produceType: 'internal'
// })
// yaml.proxies.unshift(...clashMetaProxies)
// $content = ProxyUtils.yaml.dump(yaml)
// { $content, $files } will be passed to the next operator
// { $content, $files, $options } will be passed to the next operator
// $content is the final content of the file
// flowUtils 为机场订阅流量信息处理工具
// 可参考 https://t.me/zhetengsha/948
// https://github.com/sub-store-org/Sub-Store/blob/31b6dd0507a9286d6ab834ec94ad3050f6bdc86b/backend/src/utils/download.js#L104
// 可参考:
// 1. https://t.me/zhetengsha/948
// context 为传入的上下文
// 有三种情况, 按需判断
// 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可)
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
@@ -91,6 +244,20 @@ function operator(proxies = [], targetPlatform, context) {
// 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
// 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称
// let name = ''
// for (const [key, value] of Object.entries(context.source)) {
// if (!key.startsWith('_')) {
// name = value.displayName || value.name
// break
// }
// }
// if (!name) {
// const collection = context.source._collection
// name = collection.displayName || collection.name
// }
// 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
// {
// "source": {
@@ -168,7 +335,7 @@ function operator(proxies = [], targetPlatform, context) {
// 参数说明
// 可参考 https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
console.log(JSON.stringify(context, null, 2))
console.log(JSON.stringify(context, null, 2));
return proxies
return proxies;
}

View File

@@ -3,7 +3,7 @@
*
* 【字体】
* 可参考https://www.dute.org/weird-fonts
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular
* serif-bold, serif-italic, serif-bold-italic, sans-serif-regular, sans-serif-bold-italic, script-regular, script-bold, fraktur-regular, fraktur-bold, monospace-regular, double-struck-bold, circle-regular, square-regular, modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代)
*
* 【示例】
* 1⃣ 设置所有格式为 "serif-bold"
@@ -31,6 +31,7 @@ function operator(proxies) {
"double-struck-bold": ["𝟘","𝟙","𝟚","𝟛","𝟜","𝟝","𝟞","𝟟","𝟠","𝟡","𝕒","𝕓","𝕔","𝕕","𝕖","𝕗","𝕘","𝕙","𝕚","𝕛","𝕜","𝕝","𝕞","𝕟","𝕠","𝕡","𝕢","𝕣","𝕤","𝕥","𝕦","𝕧","𝕨","𝕩","𝕪","𝕫","𝔸","𝔹","","𝔻","𝔼","𝔽","𝔾","","𝕀","𝕁","𝕂","𝕃","𝕄","","𝕆","","","","𝕊","𝕋","𝕌","𝕍","𝕎","𝕏","𝕐",""],
"circle-regular": ["⓪","①","②","③","④","⑤","⑥","⑦","⑧","⑨","ⓐ","ⓑ","ⓒ","ⓓ","ⓔ","ⓕ","ⓖ","ⓗ","ⓘ","ⓙ","ⓚ","ⓛ","ⓜ","ⓝ","ⓞ","ⓟ","ⓠ","ⓡ","ⓢ","ⓣ","ⓤ","ⓥ","ⓦ","ⓧ","ⓨ","ⓩ","Ⓐ","Ⓑ","Ⓒ","Ⓓ","Ⓔ","Ⓕ","Ⓖ","Ⓗ","Ⓘ","Ⓙ","Ⓚ","Ⓛ","Ⓜ","Ⓝ","Ⓞ","Ⓟ","Ⓠ","Ⓡ","Ⓢ","Ⓣ","Ⓤ","Ⓥ","Ⓦ","Ⓧ","Ⓨ","Ⓩ"],
"square-regular": ["0","1","2","3","4","5","6","7","8","9","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉","🄰","🄱","🄲","🄳","🄴","🄵","🄶","🄷","🄸","🄹","🄺","🄻","🄼","🄽","🄾","🄿","🅀","🅁","🅂","🅃","🅄","🅅","🅆","🅇","🅈","🅉"],
"modifier-letter": ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "ᵃ", "ᵇ", "ᶜ", "ᵈ", "ᵉ", "ᶠ", "ᵍ", "ʰ", "ⁱ", "ʲ", "ᵏ", "ˡ", "ᵐ", "ⁿ", "ᵒ", "ᵖ", "ᵠ", "ʳ", "ˢ", "ᵗ", "ᵘ", "ᵛ", "ʷ", "ˣ", "ʸ", "ᶻ", "ᴬ", "ᴮ", "ᶜ", "ᴰ", "ᴱ", "ᶠ", "ᴳ", "ʰ", "ᴵ", "ᴶ", "ᴷ", "ᴸ", "ᴹ", "ᴺ", "ᴼ", "ᴾ", "ᵠ", "ᴿ", "ˢ", "ᵀ", "ᵁ", "ᵛ", "ᵂ", "ˣ", "ʸ", "ᶻ"],
};
// charCode => index in `TABLE`

79
scripts/ip-flag-node.js Normal file
View File

@@ -0,0 +1,79 @@
const $ = $substore;
const {onlyFlagIP = true} = $arguments
async function operator(proxies) {
const BATCH_SIZE = 10;
let i = 0;
while (i < proxies.length) {
const batch = proxies.slice(i, i + BATCH_SIZE);
await Promise.all(batch.map(async proxy => {
if (onlyFlagIP && !ProxyUtils.isIP(proxy.server)) return;
try {
// remove the original flag
let proxyName = removeFlag(proxy.name);
// query ip-api
const countryCode = await queryIpApi(proxy);
proxyName = getFlagEmoji(countryCode) + ' ' + proxyName;
proxy.name = proxyName;
} catch (err) {
// TODO:
}
}));
await sleep(1000);
i += BATCH_SIZE;
}
return proxies;
}
async function queryIpApi(proxy) {
const ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:78.0) Gecko/20100101 Firefox/78.0";
const headers = {
"User-Agent": ua
};
const result = new Promise((resolve, reject) => {
const url =
`http://ip-api.com/json/${encodeURIComponent(proxy.server)}?lang=zh-CN`;
$.http.get({
url,
headers,
}).then(resp => {
const data = JSON.parse(resp.body);
if (data.status === "success") {
resolve(data.countryCode);
} else {
reject(new Error(data.message));
}
}).catch(err => {
console.log(err);
reject(err);
});
});
return result;
}
function getFlagEmoji(countryCode) {
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String
.fromCodePoint(...codePoints)
.replace(/🇹🇼/g, '🇨🇳');
}
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
.trim();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB