Compare commits

...

411 Commits

Author SHA1 Message Date
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
xream
b0c1157fe1 feat: 调整规则参数 2024-02-28 22:36:22 +08:00
xream
56626dabc7 Merge pull request #289 from makaspacex/master
修复IP-CIDR的option错误
2024-02-28 22:19:11 +08:00
makabaka
2a87f7b3c3 修复IP-CIDR的option错误 2024-02-28 20:06:03 +08:00
xream
81adfbc461 doc: README 2024-02-23 22:03:10 +08:00
xream
e04217c50d fix: 参数对象 2024-02-23 20:15:37 +08:00
xream
391a5aa2e4 chore: 调整默认定时为每天 23 点 55 分 2024-02-23 20:05:48 +08:00
xream
2f3b42f552 fix: Surge TUIC server-cert-fingerprint-sha256 2024-02-22 11:37:18 +08:00
xream
76302f9d53 fix: sing-box wireguard reserved 2024-02-21 19:09:23 +08:00
xream
1924e9735c feat: Surge 参数 tos allow-other-interface interface test-udp test-timeout hybrid; Stash 参数 benchmark-timeout; Surge 新增 SSH(仅密码方式), sing-box 新增 SSH 2024-02-18 05:25:31 +08:00
xream
6a8cee3cd5 feat: 节点名称为空时, 添加默认节点名称 2024-02-17 17:41:54 +08:00
xream
9b2209cc8b chore: sing-box grpc servicename 应为字符串 2024-02-17 12:03:25 +08:00
xream
c585785c50 fix: 兼容不规范的 VLESS URI 2024-02-17 11:39:04 +08:00
xream
c3e5da7ee4 fix: 兼容不规范的 VLESS URI 2024-02-17 10:50:33 +08:00
xream
2ecad8bbb8 feat: 旗帜操作(支持更多选项) 2024-02-17 08:32:37 +08:00
xream
a642213928 feat: 脚本操作完整支持 /api/file/name 的内部文件调用路径 2024-02-15 03:30:53 +08:00
xream
f85e360ea8 feat: 同步配置前, 预处理订阅, 防止同时请求过多 2024-02-14 19:51:44 +08:00
xream
1b948cdf52 doc: 补充文档 2024-02-13 15:30:13 +08:00
xream
556d5f393c feat: 带参数 includeUnsupportedProxy 时, 支持输出 Surge WireGuard Section 2024-02-13 15:21:29 +08:00
xream
a8c05207c0 chore: dev 调整 2024-02-11 01:10:01 +08:00
xream
e97fb1e6d9 fix: 兼容更多 Trojan URI 格式 2024-02-08 23:06:39 +08:00
xream
e40b9a77c4 fix: 修复 Loon UDP 参数 2024-02-07 15:14:24 +08:00
xream
df0ac8a218 fix: 缓存不合法时即刻重置 2024-02-07 01:14:52 +08:00
xream
36377f3c20 fix: 修复 Surge 协议 test-url 字段 2024-02-06 22:59:54 +08:00
xream
47f26bdac8 feat: Loon 解析器支持参数 ua=clash.meta&timeout=3000, 支持从链接重新获取 2024-02-06 21:29:10 +08:00
xream
a8a89ee2a2 fix: 修复 PassWall VLESS URI 兼容性 2024-02-06 00:30:53 +08:00
xream
199c5fb337 fix: 修复域名解析 2024-02-05 13:19:22 +08:00
xream
df505616ec chore: Stash 覆写增加图标 2024-02-05 11:40:02 +08:00
xream
c5b11f8b36 fix: 修复过滤非法节点功能 2024-02-04 01:22:27 +08:00
xream
b19b49d2fa chore: 端口号允许为 0 2024-02-03 23:59:26 +08:00
xream
395c6e4e4a chore: 在 bundle 文件顶部添加版本号 2024-02-03 22:34:58 +08:00
xream
ae1c738f70 chore: 开发流程使用 esbuild 2024-02-03 21:30:27 +08:00
xream
02d54208b0 feat: 支持 Hysteria(v1) URI 2024-02-03 17:54:37 +08:00
xream
5d3fc499ce feat: 支持 TUIC v5 URI 2024-02-03 16:17:26 +08:00
xream
d23bc7663e feat: 支持 Loon fast-open 2024-02-02 22:55:00 +08:00
xream
15704ea1c9 chore: 增加 esbuild bundle(暂不启用 仅本地使用) 2024-02-02 19:53:14 +08:00
xream
68cb393a7e feat: 更新 Surge, Loon, QX 输入的 Shadowsocks cipher 2024-02-02 13:23:59 +08:00
xream
2e99f28aa5 feat: 输出时校验 Surge, Surfboard, Loon, QX Shadowsocks cipher 2024-02-02 13:04:32 +08:00
xream
1a18e65e47 chore: VLESS HTTP 传输层增加默认 path 2024-02-02 12:44:08 +08:00
xream
bbd5341d7a chore: 脚本说明 2024-02-02 11:58:01 +08:00
xream
623802d73f chore: 增加脚本说明 2024-02-01 21:59:24 +08:00
xream
19920dbfa3 chore: 增加脚本说明 2024-02-01 21:58:30 +08:00
xream
8764e01d7e chore: 增加脚本说明 2024-02-01 21:58:17 +08:00
xream
31b6dd0507 chore: YAML 解析兼容(保持类型) 2024-01-31 03:27:07 +08:00
xream
a84007d39e chore: Clash 系输出中 tls 字段存在且不为布尔值时, 删除该字段防止客户端解析报错 2024-01-30 23:32:39 +08:00
xream
751e50bf99 chore: YAML 解析兼容 2024-01-30 22:23:57 +08:00
xream
a91f978042 feat: 远程订阅 URL 新增参数 validCheck 将检查订阅有效期和剩余流量 2024-01-30 14:14:57 +08:00
xream
1248e6b32a fix: v2ray-plugin 2024-01-30 03:24:42 +08:00
xream
d0f255d9c6 feat: h2 传输层修正 host 为数组, path 不为数组 2024-01-30 02:17:58 +08:00
xream
060415584e feat: 支持 tls 的 plugin 中跳过证书验证; Surge Shadow TLS 默认版本为 2; sing-box 支持 Shadowsocks Shadow TLS 2024-01-30 01:53:22 +08:00
xream
8a2087c53a feat: 支持更多不规范的 SS URI; 去除 Surfboard 节点名中的等号; 支持 Mihomo shadowsocks shadow-tls 2024-01-30 00:49:41 +08:00
xream
64117c50c7 feat: 支持更多不规范的 VLESS/SS URI 2024-01-29 23:38:21 +08:00
xream
6564d9497a chore: 增加定时任务默认为每天 0 点的说明 2024-01-24 15:17:45 +08:00
xream
1c03e46bbb chore: Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类,参数设置和参数说明(增加双引号 确保兼容性) 2024-01-23 12:45:02 +08:00
xream
084b385fdb chore: Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类,参数设置和参数说明 2024-01-23 04:03:35 +08:00
xream
290b9b5411 chore: 新增 Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类和参数设置 2024-01-22 04:48:34 +08:00
xream
e5c1ae9ed8 feat: 优化流量解析规则 2024-01-20 23:00:37 +08:00
xream
9b6d9d49f9 Merge pull request #275 from dnomd343/master
fix: 流量信息匹配错误
2024-01-20 22:40:13 +08:00
xream
a12adf5255 chore: 脚本操作时不使用空值合并运算符 2024-01-20 22:14:23 +08:00
Dnomd343
8682f14ee7 fix: scientific counting matching error 2024-01-20 13:16:11 +08:00
xream
b3de7a4bc5 feat: 优化调整 Gist 同步逻辑; 增加 GitLab Snippet 同步 2024-01-20 05:33:31 +08:00
xream
099ae5ad83 fix: 配置接口补齐错误处理 2024-01-20 00:50:35 +08:00
xream
c7d00ac512 feat: 域名解析支持类型和过滤 2024-01-19 21:43:54 +08:00
xream
ca0d800bbb release: backend version 2.14.184 2024-01-19 12:54:40 +08:00
xream
31b48d7a6c Merge pull request #274 from izhangxm/feat_add_proxy_convter_api
增加规则转换与协议转换API接口
2024-01-19 12:35:32 +08:00
makabaka
ab96ae9413 增加规则转换与协议转换API接口 2024-01-19 12:23:04 +08:00
xream
3fc507b576 feat: 解析并删除旧的 ws-path ws-headers 字段 2024-01-19 10:18:27 +08:00
xream
2f2dbbdb68 release: backend version 2.14.182 2024-01-18 17:17:15 +08:00
xream
1543e76841 Merge pull request #273 from izhangxm/master
修复clash规则头部有注释的情况下规则转换功能失败的问题
2024-01-18 17:07:59 +08:00
makabaka
74c4719806 fix_clashprovider_test 2024-01-18 15:09:24 +08:00
xream
b80d7f5875 feat: Clash 节点支持 fingerprint(内部转为 tls-fingerprint); 支持 Clash 配置文件中的 global-client-fingerprint 优先级低于 proxy 内的 client-fingerprint 2024-01-18 12:14:35 +08:00
xream
779950ab11 Revert "fix: sing-box fingerprint"
This reverts commit 42404537e8.
2024-01-18 11:36:07 +08:00
xream
42404537e8 fix: sing-box fingerprint 2024-01-18 11:29:15 +08:00
xream
228566116d feat: 支持同步配置时选择包含官方/商店版不支持的协议; 同步配置优化 2024-01-18 06:18:05 +08:00
xream
9bb06bf438 feat: 兼容不规范的 VLESS URI 2024-01-18 01:17:06 +08:00
xream
88e52f9787 revert: 去除 Loon Trojan HTTP 传输层 2024-01-17 22:13:54 +08:00
xream
845a173738 chore: README 2024-01-17 21:55:56 +08:00
xream
4a6bcbc9b4 chore: README 2024-01-17 21:49:56 +08:00
xream
bbaac2de6f fix: Loon 传输层 2024-01-17 21:24:17 +08:00
xream
614438ae3d feat: 支持 QX VLESS 输出(不支持 XTLS/REALITY) 2024-01-17 21:16:34 +08:00
xream
4966132397 feat: produceArtifact 支持 Stash internal (Fixes #271) 2024-01-17 20:31:43 +08:00
xream
059c4bd148 chore: README 2024-01-17 19:55:22 +08:00
xream
63887e3dad feat: 支持解析 QX VLESS 输入; VLESS 无 network 时, 默认为 tcp 2024-01-17 19:30:23 +08:00
xream
7fd585b5d4 feat: SurgeMac 支持 external 2024-01-17 09:15:33 +08:00
xream
16c79ac0fc feat: 支持从 gist 获取不在同步配置中的 gist 文件 2024-01-17 01:10:54 +08:00
xream
14d9885db8 fix: 不上传没有设置来源的同步配置 2024-01-16 23:41:51 +08:00
xream
1e61088ed8 chore: README 2024-01-16 20:52:46 +08:00
xream
af6904ea50 feat: 取消 github 用户名绑定关系(现在用户名错误只影响头像), 增加最近一次 gist 检查状态 2024-01-16 09:44:02 +08:00
xream
1bc44ccde8 feat: 订阅链接可使用标准参数格式 #noCache&noFlow 或 井号附加 #noCache#noFlow 2024-01-16 08:11:34 +08:00
xream
bdc7ee50f7 fix: 修复 sing-box wireguard 输出 2024-01-16 07:24:30 +08:00
xream
812f24d102 feat: 以 #noFlow 结尾的远程链接不查询订阅流量信息 2024-01-16 07:07:55 +08:00
xream
8c943176a5 feat: VLESS URI 输入兼容 Shadowrocket 导出格式 2024-01-16 01:00:22 +08:00
xream
f4c4cdba67 fix: 修复响应头缓存 2024-01-14 23:44:15 +08:00
xream
ada03be05f chore: Loon 插件支持修改响应头缓存有效期 2024-01-14 23:39:27 +08:00
xream
5584225413 feat: 优化订阅流量获取, 启用共享缓存(默认一分钟) 并优先尝试 HEAD 方法 2024-01-14 23:37:55 +08:00
xream
5cbcf4fce4 feat: Node.js 版本体支持定时任务, 环境变量 SUB_STORE_BACKEND_CRON 2024-01-14 18:45:31 +08:00
xream
89931c0032 chore: 文案调整 2024-01-14 15:46:56 +08:00
xream
88f3198320 fix: 找不到资源时报错; 调整脚本操作类型判断; 执行脚本失败时, 同时输出普通脚本和快捷脚本错误 2024-01-14 15:45:08 +08:00
xream
27a14bb255 revert: 回滚文件模板功能 2024-01-14 12:33:36 +08:00
xream
5ecce27f4e feat: 脚本内部 produceArtifact 支持指定 produceType: 'internal', produceOpts: { 'include-unsupported-proxy': true } 来获得内部的数据结构; 订阅链接参数支持 type=internal&includeUnsupportedProxy=true; 文件支持 nunjucks 模板, 为 sing-box 增加的 Filter 用法 sub/col 为订阅/组合订阅中的节点名 {{ '订阅的name' | sub('美国|🇺🇸|us', 'i') }}, subNode/colNode 为订阅/组合订阅中的节点 {{ '订阅的name' | subNode('美国|🇺🇸|us', 'i') }}, 底层 produceArtifact('subscription', 'sing-box', 'internal', '美国|🇺🇸|us', 'i') 2024-01-14 12:13:29 +08:00
xream
12903d77f7 fix: 修复无脚本操作时 文件数据结构错误 2024-01-13 22:01:02 +08:00
xream
0c6ec7f82a revert: 同步接口支持 POST 2024-01-13 20:40:45 +08:00
xream
ba251ced34 fix: 同步接口支持 POST 2024-01-13 19:56:30 +08:00
xream
d96a0421f7 fix: Surge Snell TFO 2024-01-13 16:51:48 +08:00
xream
aff7ddf41e feat: 脚本筛选的快捷操作支持 await 2024-01-13 13:55:07 +08:00
xream
164ae9a7a8 feat: 快捷脚本支持 await; 脚本操作支持 produceArtifact 2024-01-13 13:40:34 +08:00
xream
3aacd26b79 feat: 支持输出到 sing-box; 文件脚本支持 ` ; 脚本支持 ProxyUtils.yaml` 2024-01-13 10:28:07 +08:00
xream
5915416232 feat: 文件支持远程/合并, /api/file/name 接口支持参数覆盖 2024-01-12 07:22:25 +08:00
xream
c059296224 feat: 文件支持脚本操作 2024-01-12 06:16:39 +08:00
xream
9ae70eca09 feat: 同步配置支持文件 2024-01-12 03:52:41 +08:00
xream
d0acf49b83 feat: 文件接口 2024-01-12 02:23:57 +08:00
xream
c51f3511dd fix: 兼容部分不带节点名的 URI 2024-01-08 09:44:53 +08:00
xream
ee2fcc7ee3 fix: 兼容部分不带参数的 URI 输入 2024-01-08 09:28:33 +08:00
xream
95615d1877 feat: 支持全局请求超时(前端 > 2.14.29) 2024-01-08 07:22:03 +08:00
xream
962bcda9dd chore: 同步远程配置输出更多日志 2024-01-07 17:44:03 +08:00
xream
9bb4739d56 Node.js 版的通知支持第三方推送服务. 环境变量名 SUB_STORE_PUSH_SERVICE. 支持 Bark/PushPlus 等服务. 形如: https://api.day.app/XXXXXXXXX/[推送标题]/[推送内容]?group=SubStore&autoCopy=1&isArchive=1&sound=shake&level=timeSensitivehttp://www.pushplus.plus/send?token=XXXXXXXXX&title=[推送标题]&content=[推送内容]&channel=wechat 的 URL, [推送标题][推送内容] 会被自动替换 2024-01-02 22:52:33 +08:00
xream
de1d40f41a feat: Wireguard 结构跟进 Clash.Meta, allowed_ips 改为 allowed-ips 2024-01-02 16:38:48 +08:00
xream
c0ab301160 feat: Trojan URI 支持 gRPC 2023-12-29 16:08:02 +08:00
xream
a22df97a51 release: backend version 2.14.135 2023-12-29 15:42:31 +08:00
xream
45772ade4d Merge pull request #263 from Ariesly/ipv6-uri
fix: Handles node-info IPv6 address URIs
2023-12-29 15:39:03 +08:00
Ariesly
e8dab545f5 fix: Handles node-info IPv6 address URIs 2023-12-29 07:10:47 +00:00
xream
c2bd80207a doc: 补充文档 2023-12-27 02:55:04 +08:00
xream
bc5ae9a2ef feat: 支持 Surfboard(前端 > 2.14.27) 2023-12-27 00:28:15 +08:00
xream
36db057e32 feat: 当节点端口号为合法端口号时, 将类型转为整数(便于脚本判断) 2023-12-23 21:02:39 +08:00
xream
5ac73b863a feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:42:33 +08:00
xream
23042c33d6 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:41:37 +08:00
xream
4ca5f5e355 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:24:48 +08:00
xream
f10e5913fb feat: 兼容部分不规范的机场 Hysteria/Hysteria2 端口跳跃字段为空时 删除此字段 2023-12-17 18:31:12 +08:00
xream
8b75c11587 feat: Hysteria2 URI 输入支持 hy2:// 2023-12-17 16:13:34 +08:00
xream
c287dcad3b fix: 过滤 Stash/Clash Shadowsocks cipher 2023-12-13 20:11:36 +08:00
xream
ce6cd794c8 feat: 环境变量 SUB_STORE_DATA_URL 启动时自动从此地址拉取并恢复数据 2023-12-13 09:54:57 +08:00
xream
e05475aa5e feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 02:04:24 +08:00
xream
c35e9d37ae feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 01:26:16 +08:00
xream
8f2dbfe3df feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 00:34:08 +08:00
xream
a0a998dfdd feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:11 +08:00
xream
12491ac7c0 feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:03 +08:00
xream
78e3024cec feat: Node.js 前端代理后端路由 2023-12-12 22:52:50 +08:00
xream
5e21a20e37 fix: 修复 Loon Trojan WS 传输层 2023-12-12 21:13:17 +08:00
xream
76b5dc5809 feat: 脚本筛选支持节点快捷脚本. 语法与 Shadowrocket 脚本类似
```
const port = Number($server.port)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-22 06:17:45 +00:00
Hsiaoyi
6e9c3ead4c Merge pull request #217 from Keywos/script-Cache
[+] version 2.14.0
2023-07-22 14:15:56 +08:00
K
b3ccd5743a Merge branch 'sub-store-org:master' into script-Cache 2023-07-22 14:13:09 +08:00
K
e18c215fe4 [+] version 2023-07-22 14:11:45 +08:00
Hsiaoyi
e4b54b43a1 Merge pull request #216 from Keywos/script-Cache
script-Cache
2023-07-22 13:56:54 +08:00
K
21726bf950 script-Cache 2023-07-22 13:53:47 +08:00
Hsiaoyi
f6ca9af00f fix: tasks cache in Node.js environment (#209) 2023-05-09 17:16:35 +08:00
Hsiaoyi
39b79b6ca4 feat: Added support for producing Surge nodes with test-url (#199) 2023-03-19 18:32:34 +08:00
NobyDa
999271fa9d Improve resource cache key. (#190) 2023-02-08 12:30:26 +08:00
QuentinHsu
5de35c7720 🐞 fix(subscriptions): negative usage flow (#175) 2022-10-25 00:07:23 +08:00
Jasonzza
06d0c14abc fix: sync artifacts issue (#164) 2022-09-11 23:52:51 +08:00
Peng-YM
029900085c fix: cron-sync-artifacts.js path 2022-09-10 11:47:34 +08:00
91 changed files with 13648 additions and 4308 deletions

View File

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

9
.gitignore vendored
View File

@@ -127,4 +127,11 @@ out
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?
# Dist files
backend/dist/*
!backend/dist/.gitkeep
backend/sub-store.min.js
CHANGELOG.md

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "web"]
path = web
url = https://github.com/sub-store-org/Sub-Store-Front-End.git

View File

@@ -1,17 +1,17 @@
<div align="center">
<br>
<img width="200" src="https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" alt="Sub-Store">
<img width="200" src="https://raw.githubusercontent.com/cc63/ICON/main/Sub-Store.png" alt="Sub-Store">
<br>
<br>
<h2 align="center">Sub-Store<h2>
</div>
<p align="center" color="#6a737d">
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
</p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/Sub-Store)
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
<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:
@@ -19,27 +19,39 @@ Core functionalities:
1. Conversion among various formats.
2. Subscription formatting.
3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion
### Supported Input Formats
- [x] SS URI
- [x] SSR URI
- [x] SSD URI
- [x] V2RayN URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP)
- [x] Surge (SS, VMess, Trojan, HTTP)
- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP)
- [x] URI(SS, SSR, VMess, VLESS, Trojan, Hysteria, Hysteria 2, TUIC v5, WireGuard)
- [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
- [x] Surge (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] 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 (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
### Supported Target Platforms
- [x] QX
- [x] Loon
- [x] Plain JSON
- [x] Stash
- [x] Clash.Meta(mihomo)
- [x] Clash
- [x] Surfboard
- [x] Surge
- [x] Stash & Clash
- [x] ShadowRocket
- [x] Loon
- [x] Shadowrocket
- [x] QX
- [x] sing-box
- [x] V2Ray
- [x] V2Ray URI
## 2. Subscription Formatting
@@ -61,26 +73,33 @@ Core functionalities:
- [x] **Regex rename operator**: replace by regex in proxy names.
- [x] **Regex delete operator**: delete by regex in proxy names.
- [x] **Script operator**: modify proxy by script.
- [x] **Resolve Domain Operator**: resolve the domain of nodes to an IP address.
### Development
Go to `backend` and `web` directories, install node dependencies:
Install `pnpm`
Go to `backend` directories, install node dependencies:
```
npm install
pnpm i
```
1. In `backend`, run the backend server on http://localhost:3000
babel(old school)
```
npm run serve
pnpm start
```
2. In`web`, start the vue-cli server
```
npm start
```
or
esbuild(experimental)
```
pnpm run --parallel "/^dev:.*/"
```
## LICENSE
@@ -88,7 +107,12 @@ This project is under the GPL V3 LICENSE.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!
- Speicial thanks to @Orz-3 and @58xinian for their awesome icons.
- Special thanks to @Orz-3 and @58xinian for their awesome icons.

View File

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

77
backend/bundle-esbuild.js Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { build } = require('esbuild');
!(async () => {
const version = JSON.parse(
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
).version.trim();
const artifacts = [
{ src: 'src/main.js', dest: 'sub-store.min.js' },
{
src: 'src/products/resource-parser.loon.js',
dest: 'dist/sub-store-parser.loon.min.js',
},
{
src: 'src/products/cron-sync-artifacts.js',
dest: 'dist/cron-sync-artifacts.min.js',
},
{ src: 'src/products/sub-store-0.js', dest: 'dist/sub-store-0.min.js' },
{ src: 'src/products/sub-store-1.js', dest: 'dist/sub-store-1.min.js' },
];
for await (const artifact of artifacts) {
await build({
entryPoints: [artifact.src],
bundle: true,
minify: true,
sourcemap: false,
platform: 'browser',
format: 'iife',
outfile: artifact.dest,
});
}
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
encoding: 'utf8',
});
content = content.replace(
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
'$2',
);
fs.writeFileSync(
path.join(__dirname, 'dist/sub-store.no-bundle.js'),
content,
{
encoding: 'utf8',
},
);
await build({
entryPoints: ['dist/sub-store.no-bundle.js'],
bundle: true,
minify: true,
sourcemap: false,
platform: 'node',
format: 'cjs',
outfile: 'dist/sub-store.bundle.js',
});
fs.writeFileSync(
path.join(__dirname, 'dist/sub-store.bundle.js'),
`// SUB_STORE_BACKEND_VERSION: ${version}
${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
encoding: 'utf8',
})}`,
{
encoding: 'utf8',
},
);
})()
.catch((e) => {
console.log(e);
})
.finally(() => {
console.log('done');
});

51
backend/bundle.js Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { build } = require('esbuild');
!(async () => {
const version = JSON.parse(
fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'),
).version.trim();
let content = fs.readFileSync(path.join(__dirname, 'sub-store.min.js'), {
encoding: 'utf8',
});
content = content.replace(
/eval\(('|")(require\(('|").*?('|")\))('|")\)/g,
'$2',
);
fs.writeFileSync(
path.join(__dirname, 'dist/sub-store.no-bundle.js'),
content,
{
encoding: 'utf8',
},
);
await build({
entryPoints: ['dist/sub-store.no-bundle.js'],
bundle: true,
minify: true,
sourcemap: true,
platform: 'node',
format: 'cjs',
outfile: 'dist/sub-store.bundle.js',
});
fs.writeFileSync(
path.join(__dirname, 'dist/sub-store.bundle.js'),
`// SUB_STORE_BACKEND_VERSION: ${version}
${fs.readFileSync(path.join(__dirname, 'dist/sub-store.bundle.js'), {
encoding: 'utf8',
})}`,
{
encoding: 'utf8',
},
);
})()
.catch((e) => {
console.log(e);
})
.finally(() => {
console.log('done');
});

24
backend/dev-esbuild.js Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
const { build } = require('esbuild');
!(async () => {
const artifacts = [{ src: 'src/main.js', dest: 'sub-store.min.js' }];
for await (const artifact of artifacts) {
await build({
entryPoints: [artifact.src],
bundle: true,
minify: false,
sourcemap: false,
platform: 'node',
format: 'cjs',
outfile: artifact.dest,
});
}
})()
.catch((e) => {
console.log(e);
})
.finally(() => {
console.log('done');
});

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.13.2",
"version": "2.14.320",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -8,14 +8,22 @@
"test": "gulp peggy && npx cross-env BABEL_ENV=test mocha src/test/**/*.spec.js --require @babel/register --recursive",
"serve": "node sub-store.min.js",
"start": "nodemon -w src -w package.json --exec babel-node src/main.js",
"build": "gulp"
"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",
"changelog": "conventional-changelog -p cli -i CHANGELOG.md -s"
},
"author": "Peng-YM",
"license": "GPL-3.0",
"dependencies": {
"@maxmind/geoip2-node": "^5.0.0",
"automerge": "1.0.1-preview.7",
"body-parser": "^1.19.0",
"connect-history-api-fallback": "^2.0.0",
"cron": "^3.1.6",
"express": "^4.17.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"request": "^2.88.2",
@@ -30,12 +38,13 @@
"@babel/preset-env": "^7.18.0",
"@babel/register": "^7.17.7",
"@types/gulp": "^4.0.9",
"axios": "^0.20.0",
"axios": "^0.21.2",
"babel-plugin-relative-path-import": "^2.0.1",
"babelify": "^10.0.0",
"browser-pack-flat": "^3.4.2",
"browserify": "^17.0.0",
"chai": "^4.3.6",
"esbuild": "^0.19.8",
"eslint": "^8.16.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
@@ -51,4 +60,4 @@
"prettier-plugin-sort-imports": "^1.6.1",
"tinyify": "^3.0.0"
}
}
}

6901
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,10 +1,22 @@
import YAML from '@/utils/yaml';
import download from '@/utils/download';
import {
isIPv4,
isIPv6,
isValidPortNumber,
isNotBlank,
utf8ArrayToStr,
} from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors';
import PROXY_PRODUCERS from './producers';
import PROXY_PARSERS from './parsers';
import $ from '@/core/app';
import { FILES_KEY, MODULES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import { getFlag, getISO, MMDB } from '@/utils/geo';
import Gist from '@/utils/gist';
function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) {
@@ -36,7 +48,7 @@ function parse(raw) {
if (lastParser) {
const [proxy, error] = tryParse(lastParser, line);
if (!error) {
proxies.push(proxy);
proxies.push(lastParse(proxy));
success = true;
}
}
@@ -46,7 +58,7 @@ function parse(raw) {
for (const parser of PROXY_PARSERS) {
const [proxy, error] = tryParse(parser, line);
if (!error) {
proxies.push(proxy);
proxies.push(lastParse(proxy));
lastParser = parser;
success = true;
$.info(`${parser.name} is activated`);
@@ -59,39 +71,85 @@ function parse(raw) {
$.error(`Failed to parse line: ${line}`);
}
}
return proxies;
}
async function process(proxies, operators = [], targetPlatform) {
async function processFn(proxies, operators = [], targetPlatform, source) {
for (const item of operators) {
// process script
let script;
const $arguments = {};
let $arguments = {};
if (item.type.indexOf('Script') !== -1) {
const { mode, content } = item.args;
if (mode === 'link') {
const url = content;
let noCache;
let url = content;
if (url.endsWith('#noCache')) {
url = url.replace(/#noCache$/, '');
noCache = true;
}
// extract link arguments
const rawArgs = url.split('#');
if (rawArgs.length > 1) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1] || true;
$arguments[key] = value;
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
url = `${url.split('#')[0]}${noCache ? '#noCache' : ''}`;
const downloadUrlMatch = url.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 this is a remote script, download it
try {
script = await download(url.split('#')[0]);
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
);
// skip the script if download failed.
continue;
if (type === 'module') {
script = item.content;
} else {
script = await produceArtifact({
type: 'file',
name,
});
}
} catch (err) {
$.error(
`Error when loading ${type}: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else {
// if this is a remote script, download it
try {
script = await download(url);
// $.info(`Script loaded: >>>\n ${script}`);
} catch (err) {
$.error(
`Error when downloading remote script: ${item.args.content}.\n Reason: ${err}`,
);
throw new Error(`无法下载脚本: ${url}`);
}
}
} else {
script = content;
@@ -103,7 +161,7 @@ async function process(proxies, operators = [], targetPlatform) {
continue;
}
$.info(
$.log(
`Applying "${item.type}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || 'None'
}`,
@@ -114,6 +172,7 @@ async function process(proxies, operators = [], targetPlatform) {
script,
targetPlatform,
$arguments,
source,
);
} else {
processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -123,24 +182,58 @@ async function process(proxies, operators = [], targetPlatform) {
return proxies;
}
function produce(proxies, targetPlatform) {
function produce(proxies, targetPlatform, type, opts = {}) {
const producer = PROXY_PRODUCERS[targetPlatform];
if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
}
const sni_off_supported = /Surge|SurgeMac|Shadowrocket/i.test(
targetPlatform,
);
// filter unsupported proxies
proxies = proxies.filter(
(proxy) =>
!(proxy.supported && proxy.supported[targetPlatform] === false),
);
$.info(`Producing proxies for target: ${targetPlatform}`);
proxies = proxies.map((proxy) => {
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'];
}
}
return proxy;
});
$.log(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
return proxies
let localPort = 10000;
let list = proxies
.map((proxy) => {
try {
return producer.produce(proxy);
let line = producer.produce(proxy, type, opts);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
} catch (err) {
$.error(
`Cannot produce proxy: ${JSON.stringify(
@@ -152,17 +245,36 @@ function produce(proxies, targetPlatform) {
return '';
}
})
.filter((line) => line.length > 0)
.join('\n');
.filter((line) => line.length > 0);
list = type === 'internal' ? list : list.join('\n');
if (
targetPlatform.startsWith('Surge') &&
proxies.length > 0 &&
proxies.every((p) => p.type === 'wireguard')
) {
list = `#!name=${proxies[0]?.subName}
#!desc=${proxies[0]?._desc ?? ''}
#!category=${proxies[0]?._category ?? ''}
${list}`;
}
return list;
} else if (producer.type === 'ALL') {
return producer.produce(proxies);
return producer.produce(proxies, type, opts);
}
}
export const ProxyUtils = {
parse,
process,
process: processFn,
produce,
isIPv4,
isIPv6,
isIP,
yaml: YAML,
getFlag,
getISO,
MMDB,
Gist,
};
function tryParse(parser, line) {
@@ -182,3 +294,151 @@ function safeMatch(parser, line) {
return false;
}
}
function lastParse(proxy) {
if (proxy.interface) {
proxy['interface-name'] = proxy.interface;
delete proxy.interface;
}
if (isValidPortNumber(proxy.port)) {
proxy.port = parseInt(proxy.port, 10);
}
if (proxy.server) {
proxy.server = `${proxy.server}`
.trim()
.replace(/^\[/, '')
.replace(/\]$/, '');
}
if (proxy.network === 'ws') {
if (!proxy['ws-opts'] && (proxy['ws-path'] || proxy['ws-headers'])) {
proxy['ws-opts'] = {};
if (proxy['ws-path']) {
proxy['ws-opts'].path = proxy['ws-path'];
}
if (proxy['ws-headers']) {
proxy['ws-opts'].headers = proxy['ws-headers'];
}
}
delete proxy['ws-path'];
delete proxy['ws-headers'];
}
if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') {
delete proxy.network;
}
}
if (['vless'].includes(proxy.type)) {
if (!proxy.network) {
proxy.network = 'tcp';
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
proxy.tls = true;
}
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
let transporthost = proxy[`${proxy.network}-opts`]?.headers?.host;
if (proxy.network === 'h2') {
if (!transporthost && transportHost) {
proxy[`${proxy.network}-opts`].headers.host = transportHost;
delete proxy[`${proxy.network}-opts`].headers.Host;
}
} else if (transporthost && !transportHost) {
proxy[`${proxy.network}-opts`].headers.Host = transporthost;
delete proxy[`${proxy.network}-opts`].headers.host;
}
}
if (proxy.network === 'h2') {
const host = proxy['h2-opts']?.headers?.host;
const path = proxy['h2-opts']?.path;
if (host && !Array.isArray(host)) {
proxy['h2-opts'].headers.host = [host];
}
if (Array.isArray(path)) {
proxy['h2-opts'].path = path[0];
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network) {
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
if (transportHost) {
proxy.sni = transportHost;
}
}
if (!proxy.sni && !isIP(proxy.server)) {
proxy.sni = proxy.server;
}
}
// 非 tls, 有 ws/http 传输层, 使用域名的节点, 将设置传输层 Host 防止之后域名解析后丢失域名(不覆盖现有的 Host)
if (
!proxy.tls &&
['ws', 'http'].includes(proxy.network) &&
!proxy[`${proxy.network}-opts`]?.headers?.Host &&
!isIP(proxy.server)
) {
proxy[`${proxy.network}-opts`] = proxy[`${proxy.network}-opts`] || {};
proxy[`${proxy.network}-opts`].headers =
proxy[`${proxy.network}-opts`].headers || {};
proxy[`${proxy.network}-opts`].headers.Host =
['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http'
? [proxy.server]
: proxy.server;
}
// 统一将 VMess 和 VLESS 的 http 传输层的 path 和 Host 处理为数组
if (['vmess', 'vless'].includes(proxy.type) && proxy.network === 'http') {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
if (transportHost && !Array.isArray(transportHost)) {
proxy[`${proxy.network}-opts`].headers.Host = [transportHost];
}
if (transportPath && !Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = [transportPath];
}
}
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
delete proxy.ports;
}
if (['vless'].includes(proxy.type)) {
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
}
if (['http'].includes(proxy.network)) {
let transportPath = proxy[`${proxy.network}-opts`]?.path;
if (!transportPath) {
if (!proxy[`${proxy.network}-opts`]) {
proxy[`${proxy.network}-opts`] = {};
}
proxy[`${proxy.network}-opts`].path = ['/'];
}
}
}
if (typeof proxy.name !== 'string') {
if (/^\d+$/.test(proxy.name)) {
proxy.name = `${proxy.name}`;
} else {
try {
if (proxy.name?.data) {
proxy.name = Buffer.from(proxy.name.data).toString('utf8');
} else {
proxy.name = utf8ArrayToStr(proxy.name);
}
} catch (e) {
$.error(`proxy.name decode failed\nReason: ${e}`);
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
}
}
if (['', 'off'].includes(proxy.sni)) {
proxy['disable-sni'] = true;
}
return proxy;
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ const grammars = String.raw`
}
}
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) {
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
return proxy;
}
@@ -44,7 +44,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
// handle ssr obfs
proxy.obfs = obfs.type;
}
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
proxy.type = "ss";
// handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -68,6 +68,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan";
handleTransport();
}
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "http";
proxy.tls = true;
@@ -75,6 +78,9 @@ https = tag equals "https"i address (username password)? (tls_host/tls_verificat
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "socks5";
}
address = comma server:server comma port:port {
proxy.server = server;
@@ -114,7 +120,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
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");
username = & {
let j = peg$currPos;
@@ -142,6 +148,9 @@ username = & {
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
@@ -161,12 +170,15 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
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; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _
equals = _ "=" _

View File

@@ -33,7 +33,7 @@
}
}
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) {
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/socks5/hysteria2) {
return proxy;
}
@@ -42,7 +42,7 @@ shadowsocksr = tag equals "shadowsocksr"i address method password (ssr_protocol/
// handle ssr obfs
proxy.obfs = obfs.type;
}
shadowsocks = tag equals "shadowsocks"i address method password (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
shadowsocks = tag equals "shadowsocks"i address method password (obfs_typev obfs_hostv)? (obfs_ss/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
proxy.type = "ss";
// handle ss obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -66,6 +66,9 @@ trojan = tag equals "trojan"i address password (transport/transport_host/transpo
proxy.type = "trojan";
handleTransport();
}
hysteria2 = tag equals "hysteria2"i address password (tls_host/tls_verification/udp_relay/fast_open/download_bandwidth/ecn/others)* {
proxy.type = "hysteria2";
}
https = tag equals "https"i address (username password)? (tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "http";
proxy.tls = true;
@@ -73,6 +76,9 @@ https = tag equals "https"i address (username password)? (tls_host/tls_verificat
http = tag equals "http"i address (username password)? (fast_open/udp_relay/others)* {
proxy.type = "http";
}
socks5 = tag equals "socks5"i address (username password)? (over_tls/tls_host/tls_verification/fast_open/udp_relay/others)* {
proxy.type = "socks5";
}
address = comma server:server comma port:port {
proxy.server = server;
@@ -112,7 +118,7 @@ port = digits:[0-9]+ {
method = comma cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none"/"auto");
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");
username = & {
let j = peg$currPos;
@@ -140,6 +146,9 @@ username = & {
password = comma '"' match:[^"]* '"' { proxy.password = match.join(""); }
uuid = comma '"' match:[^"]+ '"' { proxy.uuid = match.join(""); }
obfs_typev = comma type:("http"/"tls") { obfs.type = type; }
obfs_hostv = comma match:[^,]+ { obfs.host = match.join(""); }
obfs_ss = comma "obfs-name" equals type:("http"/"tls") { obfs.type = type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { obfs.type = type; }
@@ -159,12 +168,15 @@ ssr_protocol_param = comma "protocol-param" equals param:$[^=,]+ { proxy["protoc
vmess_alterId = comma "alterId" equals alterId:$[0-9]+ { proxy.alterId = parseInt(alterId); }
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; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _
equals = _ "=" _

View File

@@ -38,20 +38,23 @@ const grammars = String.raw`
}
}
start = (trojan/shadowsocks/vmess/http/socks5) {
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
return proxy
}
trojan = "trojan" equals address
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* {
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
proxy.type = "trojan";
handleObfs();
}
shadowsocks = "shadowsocks" equals address
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* {
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/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;
@@ -80,7 +83,7 @@ shadowsocks = "shadowsocks" equals address
}
vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -91,13 +94,20 @@ vmess = "vmess" equals address
handleObfs();
}
vless = "vless" equals address
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vless";
proxy.cipher = proxy.cipher || "none";
handleObfs();
}
http = "http" equals address
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
proxy.type = "http";
}
socks5 = "socks5" equals address
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* {
(username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
proxy.type = "socks5";
}
@@ -142,7 +152,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
method = comma "method" equals cipher:cipher {
proxy.cipher = cipher;
};
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
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");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
@@ -155,9 +165,17 @@ tls_verification = comma "tls-verification" equals flag:bool {
proxy["skip-cert-verify"] = !flag;
}
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
proxy["tls-no-session-ticket"] = flag;
}
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
proxy["tls-no-session-reuse"] = flag;
}
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { 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; }
@@ -166,6 +184,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
uri = $[^,]+
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }

View File

@@ -36,20 +36,23 @@
}
}
start = (trojan/shadowsocks/vmess/http/socks5) {
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
return proxy
}
trojan = "trojan" equals address
(password/over_tls/tls_host/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/others)* {
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/obfs/obfs_host/obfs_uri/tag/udp_relay/udp_over_tcp/fast_open/server_check_url/others)* {
proxy.type = "trojan";
handleObfs();
}
shadowsocks = "shadowsocks" equals address
(password/method/obfs_ssr/obfs_ss/obfs_host/obfs_uri/ssr_protocol/ssr_protocol_param/tls_fingerprint/tls_verification/udp_relay/udp_over_tcp/fast_open/tag/others)* {
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/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;
@@ -78,7 +81,7 @@ shadowsocks = "shadowsocks" equals address
}
vmess = "vmess" equals address
(uuid/method/over_tls/tls_host/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/others)* {
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -89,13 +92,20 @@ vmess = "vmess" equals address
handleObfs();
}
vless = "vless" equals address
(uuid/method/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/obfs/obfs_host/obfs_uri/udp_relay/udp_over_tcp/fast_open/aead/server_check_url/others)* {
proxy.type = "vless";
proxy.cipher = proxy.cipher || "none";
handleObfs();
}
http = "http" equals address
(username/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)*{
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)*{
proxy.type = "http";
}
socks5 = "socks5" equals address
(username/password/password/over_tls/tls_host/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/others)* {
(username/password/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/others)* {
proxy.type = "socks5";
}
@@ -140,7 +150,7 @@ uuid = comma "password" equals uuid:[^=,]+ { proxy.uuid = uuid.join("").trim();
method = comma "method" equals cipher:cipher {
proxy.cipher = cipher;
};
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
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");
aead = comma "aead" equals flag:bool { proxy.aead = flag; }
udp_relay = comma "udp-relay" equals flag:bool { proxy.udp = flag; }
@@ -153,9 +163,17 @@ tls_verification = comma "tls-verification" equals flag:bool {
proxy["skip-cert-verify"] = !flag;
}
tls_fingerprint = comma "tls-cert-sha256" equals tls_fingerprint:$[^,]+ { proxy["tls-fingerprint"] = tls_fingerprint.trim(); }
tls_pubkey_sha256 = comma "tls-pubkey-sha256" equals param:$[^=,]+ { proxy["tls-pubkey-sha256"] = param; }
tls_alpn = comma "tls-alpn" equals param:$[^=,]+ { proxy["tls-alpn"] = param; }
tls_no_session_ticket = comma "tls-no-session-ticket" equals flag:bool {
proxy["tls-no-session-ticket"] = flag;
}
tls_no_session_reuse = comma "tls-no-session-reuse" equals flag:bool {
proxy["tls-no-session-reuse"] = flag;
}
obfs_ss = comma "obfs" equals type:("http"/"tls"/"wss"/"ws"/"over-tls") { obfs.type = type; return type; }
obfs_ssr = comma "obfs" equals type:("plain"/"http_simple"/"http_post"/"random_head"/"tls1.2_ticket_auth"/"tls1.2_ticket_fastauth") { 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; }
@@ -164,6 +182,8 @@ obfs_uri = comma "obfs-uri" equals uri:uri { obfs.path = uri; }
ssr_protocol = comma "ssr-protocol" equals protocol:("origin"/"auth_sha1_v4"/"auth_aes128_md5"/"auth_aes128_sha1"/"auth_chain_a"/"auth_chain_b") { proxy.protocol = protocol; return protocol; }
ssr_protocol_param = comma "ssr-protocol-param" equals param:$[^=,]+ { proxy["protocol-param"] = param; }
server_check_url = comma "server_check_url" equals param:$[^=,]+ { proxy["test-url"] = param; }
uri = $[^,]+
tag = comma "tag" equals tag:[^=,]+ { proxy.name = tag.join(""); }

View File

@@ -25,15 +25,23 @@ const grammars = String.raw`
proxy.network = "ws";
$set(proxy, "ws-opts.path", obfs.path);
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
}
}
}
function handleShadowTLS() {
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
proxy['shadow-tls-version'] = 2;
}
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/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)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -42,8 +50,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.host", obfs.host);
$set(proxy, "plugin-opts.path", obfs.path);
}
handleShadowTLS();
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/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)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -52,19 +61,27 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
proxy.alterId = proxy.alterId || 0;
}
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/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)* {
proxy.type = "trojan";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/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)* {
proxy.type = "http";
proxy.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (fast_open/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
handleShadowTLS();
}
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/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/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)* {
proxy.type = "snell";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -72,13 +89,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.host", obfs.host);
$set(proxy, "obfs-opts.path", obfs.path);
}
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (fast_open/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/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)* {
proxy.type = "tuic";
proxy.version = 5;
handleShadowTLS();
}
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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)* {
proxy.type = "socks5";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/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)* {
proxy.type = "socks5";
proxy.tls = true;
handleShadowTLS();
}
address = comma server:server comma port:port {
@@ -140,13 +177,20 @@ username = & {
password = comma match:[^,]+ { proxy.password = match.join(""); }
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(""); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -154,7 +198,7 @@ vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
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");
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
@@ -175,6 +219,31 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _

View File

@@ -23,15 +23,23 @@
proxy.network = "ws";
$set(proxy, "ws-opts.path", obfs.path);
$set(proxy, "ws-opts.headers", obfs['ws-headers']);
if (proxy['ws-opts'] && proxy['ws-opts']['headers'] && proxy['ws-opts']['headers'].Host) {
proxy['ws-opts']['headers'].Host = proxy['ws-opts']['headers'].Host.replace(/^"(.*)"$/, '$1')
}
}
}
function handleShadowTLS() {
if (proxy['shadow-tls-password'] && !proxy['shadow-tls-version']) {
proxy['shadow-tls-version'] = 2;
}
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh) {
return proxy;
}
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/others)* {
shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/ip_version/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)* {
proxy.type = "ss";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -40,8 +48,9 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
$set(proxy, "plugin-opts.host", obfs.host);
$set(proxy, "plugin-opts.path", obfs.path);
}
handleShadowTLS();
}
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/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)* {
proxy.type = "vmess";
proxy.cipher = proxy.cipher || "none";
if (proxy.aead) {
@@ -50,19 +59,27 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
proxy.alterId = proxy.alterId || 0;
}
handleWebsocket();
handleShadowTLS();
}
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/fast_open/udp_relay/others)* {
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/ip_version/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)* {
proxy.type = "trojan";
handleWebsocket();
handleShadowTLS();
}
https = tag equals "https" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
https = tag equals "https" address (username password)? (usernamek passwordk)? (sni/tls_fingerprint/tls_verification/ip_version/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)* {
proxy.type = "http";
proxy.tls = true;
handleShadowTLS();
}
http = tag equals "http" address (username password)? (fast_open/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/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "http";
handleShadowTLS();
}
snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_uri/fast_open/udp_relay/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/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)* {
proxy.type = "snell";
// handle obfs
if (obfs.type == "http" || obfs.type === "tls") {
@@ -70,13 +87,33 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
$set(proxy, "obfs-opts.host", obfs.host);
$set(proxy, "obfs-opts.path", obfs.path);
}
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (fast_open/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/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)* {
proxy.type = "tuic";
proxy.version = 5;
handleShadowTLS();
}
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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)* {
proxy.type = "socks5";
handleShadowTLS();
}
socks5_tls = tag equals "socks5-tls" address (username password)? (sni/tls_fingerprint/tls_verification/fast_open/others)* {
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (no_error_alert/ip_version/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)* {
proxy.type = "socks5";
proxy.tls = true;
handleShadowTLS();
}
address = comma server:server comma port:port {
@@ -138,13 +175,20 @@ username = & {
password = comma match:[^,]+ { proxy.password = match.join(""); }
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(""); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -152,7 +196,7 @@ vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
method = comma "encrypt-method" equals cipher:cipher {
proxy.cipher = cipher;
}
cipher = ("aes-128-gcm"/"aes-192-gcm"/"aes-256-gcm"/"aes-128-cfb"/"aes-192-cfb"/"aes-256-cfb"/"aes-128-ctr"/"aes-192-ctr"/"aes-256-ctr"/"rc4-md5"/"xchacha20-ietf-poly1305"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"none");
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");
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
@@ -173,6 +217,31 @@ uri = $[^,]+
udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
reuse = comma "reuse" equals flag:bool { proxy.reuse = flag; }
ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
test_udp = comma "test-udp" equals match:[^,]+ { proxy["test-udp"] = match.join(""); }
test_timeout = comma "test-timeout" equals match:$[0-9]+ { proxy["test-timeout"] = parseInt(match.trim()); }
tos = comma "tos" equals match:$[0-9]+ { proxy.tos = parseInt(match.trim()); }
interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(""); }
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _

View File

@@ -30,7 +30,7 @@ start = (trojan) {
return proxy
}
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
proxy.type = "trojan";
proxy.password = password;
proxy.server = server;
@@ -87,6 +87,32 @@ params = "?" head:param tail:("&"@param)* {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
let httpupgrade
proxy.network = params["type"]
if(proxy.network === 'httpupgrade') {
proxy.network = 'ws'
httpupgrade = true
}
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
if (httpupgrade) {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
@@ -94,7 +120,7 @@ params = "?" head:param tail:("&"@param)* {
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value;
}

View File

@@ -28,7 +28,7 @@ start = (trojan) {
return proxy
}
trojan = "trojan://" password:password "@" server:server ":" port:port params? name:name?{
trojan = "trojan://" password:password "@" server:server ":" port:port "/"? params? name:name?{
proxy.type = "trojan";
proxy.password = password;
proxy.server = server;
@@ -85,6 +85,32 @@ params = "?" head:param tail:("&"@param)* {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
let httpupgrade
proxy.network = params["type"]
if(proxy.network === 'httpupgrade') {
proxy.network = 'ws'
httpupgrade = true
}
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
if (httpupgrade) {
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade", true);
$set(proxy, proxy.network+"-opts.v2ray-http-upgrade-fast-open", true);
}
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
@@ -92,7 +118,7 @@ params = "?" head:param tail:("&"@param)* {
param = kv/single;
kv = key:$[a-z]i+ "=" value:$[^&#]i+ {
kv = key:$[a-z]i+ "=" value:$[^&#]i* {
params[key] = value;
}

View File

@@ -1,4 +1,4 @@
import { safeLoad } from 'static-js-yaml';
import { safeLoad } from '@/utils/yaml';
import { Base64 } from 'js-base64';
function HTML() {
@@ -13,17 +13,26 @@ function Base64Encoded() {
const name = 'Base64 Pre-processor';
const keys = [
'dm1lc3M',
'c3NyOi8v',
'dHJvamFu',
'c3M6Ly',
'c3NkOi8v',
'c2hhZG93',
'aHR0c',
'dm1lc3M', // vmess
'c3NyOi8v', // ssr://
'dHJvamFu', // trojan
'c3M6Ly', // ss:/
'c3NkOi8v', // ssd://
'c2hhZG93', // shadow
'aHR0c', // htt
'dmxlc3M=', // vless
'aHlzdGVyaWEy', // hysteria2
'aHkyOi8v', // hy2://
'd2lyZWd1YXJkOi8v', // wireguard://
'd2c6Ly8=', // wg://
'dHVpYzovLw==', // tuic://
];
const test = function (raw) {
return keys.some((k) => raw.indexOf(k) !== -1);
return (
!/^\w+:\/\/\w+/im.test(raw) &&
keys.some((k) => raw.indexOf(k) !== -1)
);
};
const parse = function (raw) {
raw = Base64.decode(raw);
@@ -35,12 +44,25 @@ function Base64Encoded() {
function Clash() {
const name = 'Clash Pre-processor';
const test = function (raw) {
return /proxies/.test(raw);
if (!/proxies/.test(raw)) return false;
const content = safeLoad(raw);
return content.proxies && Array.isArray(content.proxies);
};
const parse = function (raw) {
// Clash YAML format
const proxies = safeLoad(raw).proxies;
return proxies.map((p) => JSON.stringify(p)).join('\n');
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');
};
return { name, test, parse };
}
@@ -95,24 +117,12 @@ function FullConfig() {
return /^(\[server_local\]|\[Proxy\])/gm.test(raw);
};
const parse = function (raw) {
const regex = /^\[server_local]|\[Proxy]/gm;
const match = regex.exec(raw);
const results = [];
let first = true;
if (match) {
raw = raw.substring(match.index);
for (const line of raw.split('\n')) {
if (!first && !line.test(/^\s*\[/)) {
results.push(line);
}
// skip the first line
first = false;
}
return results.join('\n');
}
const match = raw.match(
/^\[server_local|Proxy\]([\s\S]+?)^\[.+?\](\r?\n|$)/im,
)?.[1];
return match || raw;
};
return { name, test, parse };
}
export default [HTML(), Base64Encoded(), Clash(), SSD(), FullConfig()];
export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];

View File

@@ -1,4 +1,5 @@
import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import { isIPv4, isIPv6 } from '@/utils';
import { FULL } from '@/utils/logical';
import { getFlag } from '@/utils/geo';
@@ -6,6 +7,17 @@ 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 env from '@/utils/env';
import {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
validCheck,
flowTransfer,
getRmainingDays,
} from '@/utils/flow';
/**
The rule "(name CONTAINS "🇨🇳") AND (port IN [80, 443])" can be expressed as follows:
@@ -76,12 +88,15 @@ function QuickSettingOperator(args) {
if (get(args.useless)) {
const filter = UselessFilter();
const selected = filter.func(proxies);
proxies.filter((_, i) => selected[i]);
proxies = proxies.filter(
(p, i) => selected[i] && p.port > 0 && p.port <= 65535,
);
}
return proxies.map((proxy) => {
proxy.udp = get(args.udp, proxy.udp);
proxy.tfo = get(args.tfo, proxy.tfo);
proxy['fast-open'] = get(args.tfo, proxy['fast-open']);
proxy['skip-cert-verify'] = get(
args.scert,
proxy['skip-cert-verify'],
@@ -107,7 +122,7 @@ function QuickSettingOperator(args) {
}
// add or remove flag for proxies
function FlagOperator({ mode }) {
function FlagOperator({ mode, tw }) {
return {
name: 'Flag Operator',
func: (proxies) => {
@@ -121,7 +136,13 @@ function FlagOperator({ mode }) {
// remove old flag
proxy.name = removeFlag(proxy.name);
proxy.name = newFlag + ' ' + proxy.name;
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
if (tw == 'ws') {
proxy.name = proxy.name.replace(/🇹🇼/g, '🇼🇸');
} else if (tw == 'tw') {
// 不变
} else {
proxy.name = proxy.name.replace(/🇹🇼/g, '🇨🇳');
}
}
return proxy;
});
@@ -293,7 +314,7 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`!
2. Always declare variables before using them!
*/
function ScriptOperator(script, targetPlatform, $arguments) {
function ScriptOperator(script, targetPlatform, $arguments, source) {
return {
name: 'Script Operator',
func: async (proxies) => {
@@ -304,22 +325,78 @@ function ScriptOperator(script, targetPlatform, $arguments) {
script,
$arguments,
);
output = operator(proxies, targetPlatform);
output = operator(proxies, targetPlatform, { source, ...env });
})();
return output;
},
nodeFunc: async (proxies) => {
let output = proxies;
await (async function () {
const operator = createDynamicFunction(
'operator',
`async function operator(input = []) {
if (input && (input.$files || input.$content)) {
let { $content, $files } = input
${script}
return { $content, $files }
} else {
let proxies = input
let list = []
for await (let $server of proxies) {
${script}
list.push($server)
}
return list
}
}`,
$arguments,
);
output = operator(proxies, targetPlatform, { source, ...env });
})();
return output;
},
};
}
function parseIP4P(IP4P) {
let server;
let port;
try {
if (!/^2001::[^:]+:[^:]+:[^:]+$/.test(IP4P)) {
throw new Error(`Invalid IP4P: ${IP4P}`);
}
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}`);
}
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 = {
Google: async function (domain) {
const id = hex_md5(`GOOGLE:${domain}`);
Google: async function (domain, type, noCache) {
const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `https://8.8.4.4/resolve?name=${encodeURIComponent(
domain,
)}&type=A`,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
headers: {
accept: 'application/dns-json',
},
@@ -336,10 +413,13 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
'IP-API': async function (domain) {
'IP-API': async function (domain, type, noCache) {
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,
@@ -353,14 +433,14 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Cloudflare: async function (domain) {
const id = hex_md5(`CLOUDFLARE:${domain}`);
Cloudflare: async function (domain, type, noCache) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id);
if (cached) return cached;
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent(
domain,
)}&type=A`,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`,
headers: {
accept: 'application/dns-json',
},
@@ -377,12 +457,57 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result);
return result;
},
Ali: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://223.6.6.6/resolve?edns_client_subnet=223.6.6.6/24&name=${encodeURIComponent(
domain,
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}&short=1`,
headers: {
accept: 'application/dns-json',
},
});
const answers = JSON.parse(resp.body);
if (answers.length === 0) {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
Tencent: async function (domain, type, noCache) {
const id = hex_md5(`ALI:${domain}:${type}`);
const cached = resourceCache.get(id);
if (!noCache && cached) return cached;
const resp = await $.http.get({
url: `http://119.28.28.28/d?ip=119.28.28.28&type=${
type === 'IPv6' ? 'AAAA' : 'A'
}&dn=${encodeURIComponent(domain)}`,
headers: {
accept: 'application/dns-json',
},
});
const answers = resp.body.split(';').map((i) => i.split(',')[0]);
if (answers.length === 0 || String(answers) === '0') {
throw new Error('No answers');
}
const result = answers[answers.length - 1];
resourceCache.set(id, result);
return result;
},
};
function ResolveDomainOperator({ provider }) {
function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
}
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
const resolver = DOMAIN_RESOLVERS[provider];
if (!resolver) {
throw new Error(`Cannot find resolver: ${provider}`);
throw new Error(`找不到域名解析服务提供方: ${provider}`);
}
return {
name: 'Resolve Domain Operator',
@@ -391,7 +516,9 @@ function ResolveDomainOperator({ provider }) {
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
const totalDomain = [
...new Set(
proxies.filter((p) => !isIP(p.server)).map((c) => c.server),
proxies
.filter((p) => !isIP(p.server) && !p['no-resolve'])
.map((c) => c.server),
),
];
const totalBatch = Math.ceil(totalDomain.length / limit);
@@ -399,7 +526,7 @@ function ResolveDomainOperator({ provider }) {
const currentBatch = [];
for (let domain of totalDomain.splice(0, limit)) {
currentBatch.push(
resolver(domain)
resolver(domain, type, cache === 'disabled')
.then((ip) => {
results[domain] = ip;
$.info(
@@ -415,11 +542,53 @@ function ResolveDomainOperator({ provider }) {
}
await Promise.all(currentBatch);
}
proxies.forEach((proxy) => {
proxy.server = results[proxy.server] || proxy.server;
proxies.forEach((p) => {
if (!p['no-resolve']) {
if (results[p.server]) {
if (_type === 'IP4P') {
const { server, port } = parseIP4P(
results[p.server],
);
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 {
p.resolved = false;
}
} else {
p._domain = p.server;
p.server = results[p.server];
p.resolved = true;
p[`_${type}`] = p.server;
if (!isIP(p._IP)) {
p._IP = p.server;
}
}
} else {
p.resolved = false;
}
}
});
return proxies;
return proxies.filter((p) => {
if (filter === 'removeFailed') {
return isIP(p.server) || p['no-resolve'] || p.resolved;
} else if (filter === 'IPOnly') {
return isIP(p.server);
} else if (filter === 'IPv4Only') {
return isIPv4(p.server);
} else if (filter === 'IPv6Only') {
return isIPv6(p.server);
} else {
return true;
}
});
},
};
}
@@ -460,6 +629,8 @@ function RegionFilter(regions) {
SG: '🇸🇬',
JP: '🇯🇵',
UK: '🇬🇧',
DE: '🇩🇪',
KR: '🇰🇷',
};
return {
name: 'Region Filter',
@@ -521,7 +692,7 @@ function TypeFilter(types) {
1. This function name should be `filter`!
2. Always declare variables before using them!
*/
function ScriptFilter(script, targetPlatform, $arguments) {
function ScriptFilter(script, targetPlatform, $arguments, source) {
return {
name: 'Script Filter',
func: async (proxies) => {
@@ -532,7 +703,29 @@ function ScriptFilter(script, targetPlatform, $arguments) {
script,
$arguments,
);
output = filter(proxies, targetPlatform);
output = filter(proxies, targetPlatform, { source, ...env });
})();
return output;
},
nodeFunc: async (proxies) => {
let output = FULL(proxies.length, true);
await (async function () {
const filter = createDynamicFunction(
'filter',
`async function filter(input = []) {
let proxies = input
let list = []
const fn = async ($server) => {
${script}
}
for await (let $server of proxies) {
list.push(await fn($server))
}
return list
}`,
$arguments,
);
output = filter(proxies, targetPlatform, { source, ...env });
})();
return output;
},
@@ -564,8 +757,32 @@ async function ApplyFilter(filter, objs) {
try {
selected = await filter.func(objs);
} catch (err) {
// print log and skip this filter
$.log(`Cannot apply filter ${filter.name}\n Reason: ${err}`);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (funcErrMsg.includes('$server is not defined')) {
funcErr = '';
} else {
$.error(
`Cannot apply filter ${filter.name}(function filter)! Reason: ${err}`,
);
funcErr = `执行 function filter 失败 ${funcErrMsg}; `;
}
try {
selected = await filter.nodeFunc(objs);
} catch (err) {
$.error(
`Cannot apply filter ${filter.name}(shortcut script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
if (funcErr && nodeErrMsg === funcErrMsg) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行快捷过滤脚本 失败 ${nodeErrMsg}`;
}
throw new Error(`脚本过滤 ${funcErr}${nodeErr}`);
}
}
return objs.filter((_, i) => selected[i]);
}
@@ -576,8 +793,39 @@ async function ApplyOperator(operator, objs) {
const output_ = await operator.func(output);
if (output_) output = output_;
} catch (err) {
// print log and skip this operator
$.log(`Cannot apply operator ${operator.name}! Reason: ${err}`);
let funcErr = '';
let funcErrMsg = `${err.message ?? err}`;
if (
funcErrMsg.includes('$server is not defined') ||
funcErrMsg.includes('$content is not defined') ||
funcErrMsg.includes('$files is not defined') ||
output?.$files ||
output?.$content
) {
funcErr = '';
} else {
$.error(
`Cannot apply operator ${operator.name}(function operator)! Reason: ${err}`,
);
funcErr = `执行 function operator 失败 ${funcErrMsg}; `;
}
try {
const output_ = await operator.nodeFunc(output);
if (output_) output = output_;
} catch (err) {
$.error(
`Cannot apply operator ${operator.name}(shortcut script)! Reason: ${err}`,
);
let nodeErr = '';
let nodeErrMsg = `${err.message ?? err}`;
if (funcErr && nodeErrMsg === funcErrMsg) {
nodeErr = '';
funcErr = `执行失败 ${funcErrMsg}`;
} else {
nodeErr = `执行快捷脚本 失败 ${nodeErrMsg}`;
}
throw new Error(`脚本操作 ${funcErr}${nodeErr}`);
}
}
return output;
}
@@ -619,11 +867,19 @@ function clone(object) {
// remove flag
function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g, '')
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')
.trim();
}
function createDynamicFunction(name, script, $arguments) {
const flowUtils = {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
flowTransfer,
validCheck,
getRmainingDays,
};
if ($.env.isLoon) {
return new Function(
'$arguments',
@@ -633,6 +889,9 @@ function createDynamicFunction(name, script, $arguments) {
'$httpClient',
'$notification',
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)(
$arguments,
@@ -645,6 +904,9 @@ function createDynamicFunction(name, script, $arguments) {
// eslint-disable-next-line no-undef
$notification,
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
} else {
return new Function(
@@ -652,7 +914,19 @@ function createDynamicFunction(name, script, $arguments) {
'$substore',
'lodash',
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils);
)(
$arguments,
$,
lodash,
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
}
}

View File

@@ -2,36 +2,181 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
const produce = (proxies, type, opts = {}) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies
proxies = proxies.filter((proxy) =>
['ss', 'ssr', 'vmess', 'socks', 'http', 'snell', 'trojan'].includes(
proxy.type,
),
);
return (
'proxies:\n' +
proxies
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
// https://clash.wiki/configuration/outbound.html#shadowsocks
const list = proxies
.filter((proxy) => {
if (opts['include-unsupported-proxy']) return true;
if (
![
'ss',
'ssr',
'vmess',
'vless',
'socks5',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type) ||
(proxy.type === 'ss' &&
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'rc4-md5',
'chacha20-ietf',
'xchacha20',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
}
return true;
})
.map((proxy) => {
if (proxy.type === 'vmess') {
// handle vmess aead
if (isPresent(proxy, 'aead')) {
if (proxy.aead) {
proxy.alterId = 0;
}
delete proxy.aead;
}
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
// https://dreamacro.github.io/clash/configuration/outbound.html#vmess
if (
isPresent(proxy, 'cipher') &&
![
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(proxy.cipher)
) {
proxy.cipher = 'auto';
}
} else if (proxy.type === 'wireguard') {
proxy.keepalive =
proxy.keepalive ?? proxy['persistent-keepalive'];
proxy['persistent-keepalive'] = proxy.keepalive;
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
delete proxy['tls-fingerprint'];
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('')
);
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'http'
) {
let httpPath = proxy['http-opts']?.path;
if (
isPresent(proxy, 'http-opts.path') &&
!Array.isArray(httpPath)
) {
proxy['http-opts'].path = [httpPath];
}
let httpHost = proxy['http-opts']?.headers?.Host;
if (
isPresent(proxy, 'http-opts.headers.Host') &&
!Array.isArray(httpHost)
) {
proxy['http-opts'].headers.Host = [httpHost];
}
}
if (
['vmess', 'vless'].includes(proxy.type) &&
proxy.network === 'h2'
) {
let path = proxy['h2-opts']?.path;
if (
isPresent(proxy, 'h2-opts.path') &&
Array.isArray(path)
) {
proxy['h2-opts'].path = path[0];
}
let host = proxy['h2-opts']?.headers?.host;
if (
isPresent(proxy, 'h2-opts.headers.Host') &&
!Array.isArray(host)
) {
proxy['h2-opts'].headers.host = [host];
}
}
if (proxy['plugin-opts']?.tls) {
if (isPresent(proxy, 'skip-cert-verify')) {
proxy['plugin-opts']['skip-cert-verify'] =
proxy['skip-cert-verify'];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(
proxy.type,
)
) {
delete proxy.tls;
}
if (proxy['tls-fingerprint']) {
proxy.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
if (proxy['underlying-proxy']) {
proxy['dialer-proxy'] = proxy['underlying-proxy'];
}
delete proxy['underlying-proxy'];
if (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
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'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

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

View File

@@ -1,22 +1,37 @@
import Surge_Producer from './surge';
import SurgeMac_Producer from './surgemac';
import Clash_Producer from './clash';
import ClashMeta_Producer from './clashmeta';
import Stash_Producer from './stash';
import Loon_Producer from './loon';
import URI_Producer from './uri';
import V2Ray_Producer from './v2ray';
import QX_Producer from './qx';
import Shadowrocket_Producer from './shadowrocket';
import Surfboard_Producer from './surfboard';
import singbox_Producer from './sing-box';
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(),
QuantumultX: QX_Producer(),
Surge: Surge_Producer(),
SurgeMac: SurgeMac_Producer(),
Loon: Loon_Producer(),
Clash: Clash_Producer(),
ClashMeta: ClashMeta_Producer(),
URI: URI_Producer(),
V2Ray: V2Ray_Producer(),
JSON: JSON_Producer(),
Stash: Stash_Producer(),
Shadowrocket: Shadowrocket_Producer(),
ShadowRocket: Shadowrocket_Producer(),
Surfboard: Surfboard_Producer(),
'sing-box': singbox_Producer(),
};

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-case-declarations */
const targetPlatform = 'Loon';
import { isPresent, Result } from './utils';
import { isIPv4, isIPv6 } from '@/utils';
export default function Loon_Producer() {
const produce = (proxy) => {
@@ -17,6 +18,12 @@ export default function Loon_Producer() {
return vless(proxy);
case 'http':
return http(proxy);
case 'socks5':
return socks5(proxy);
case 'wireguard':
return wireguard(proxy);
case 'hysteria2':
return hysteria2(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -27,6 +34,32 @@ export default function Loon_Producer() {
function shadowsocks(proxy) {
const result = new Result(proxy);
if (
![
'rc4',
'rc4-md5',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'bf-cfb',
'camellia-128-cfb',
'camellia-192-cfb',
'camellia-256-cfb',
'salsa20',
'chacha20',
'chacha20-ietf',
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
result.append(
`${proxy.name}=shadowsocks,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.password}"`,
);
@@ -52,7 +85,9 @@ function shadowsocks(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString();
}
@@ -78,7 +113,9 @@ function shadowsocksr(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString();
}
@@ -88,17 +125,19 @@ function trojan(proxy) {
result.append(
`${proxy.name}=trojan,${proxy.server},${proxy.port},"${proxy.password}"`,
);
if (proxy.network === 'tcp') {
delete proxy.network;
}
// transport
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,transport=ws`);
result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`,
`,path=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`,
`,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else {
@@ -119,7 +158,9 @@ function trojan(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString();
}
@@ -127,31 +168,33 @@ function trojan(proxy) {
function vmess(proxy) {
const result = new Result(proxy);
result.append(
`${proxy.name}=vmess,${proxy.server},${proxy.port},${
proxy.cipher === 'auto' ? 'none' : proxy.cipher
},"${proxy.uuid}"`,
`${proxy.name}=vmess,${proxy.server},${proxy.port},${proxy.cipher},"${proxy.uuid}"`,
);
if (proxy.network === 'tcp') {
delete proxy.network;
}
// transport
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,transport=ws`);
result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`,
`,path=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`,
`,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else if (proxy.network === 'http') {
result.append(`,transport=http`);
let httpPath = proxy['http-opts']?.path;
let httpHost = proxy['http-opts']?.headers?.Host;
result.appendIfPresent(
`,path=${proxy['http-opts'].path}`,
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
'http-opts.path',
);
result.appendIfPresent(
`,host=${proxy['http-opts'].headers.Host}`,
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
'http-opts.headers.Host',
);
} else {
@@ -184,36 +227,45 @@ function vmess(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString();
}
function vless(proxy) {
if (proxy['reality-opts']) {
throw new Error(`VLESS REALITY is unsupported`);
}
const result = new Result(proxy);
result.append(
`${proxy.name}=vless,${proxy.server},${proxy.port},"${proxy.uuid}"`,
);
if (proxy.network === 'tcp') {
delete proxy.network;
}
// transport
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,transport=ws`);
result.appendIfPresent(
`,path=${proxy['ws-opts'].path}`,
`,path=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
result.appendIfPresent(
`,host=${proxy['ws-opts'].headers.Host}`,
`,host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else if (proxy.network === 'http') {
result.append(`,transport=http`);
let httpPath = proxy['http-opts']?.path;
let httpHost = proxy['http-opts']?.headers?.Host;
result.appendIfPresent(
`,path=${proxy['http-opts'].path}`,
`,path=${Array.isArray(httpPath) ? httpPath[0] : httpPath}`,
'http-opts.path',
);
result.appendIfPresent(
`,host=${proxy['http-opts'].headers.Host}`,
`,host=${Array.isArray(httpHost) ? httpHost[0] : httpHost}`,
'http-opts.headers.Host',
);
} else {
@@ -239,7 +291,9 @@ function vless(proxy) {
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp=${proxy.udp}`, 'udp');
if (proxy.udp) {
result.append(`,udp=true`);
}
return result.toString();
}
@@ -262,7 +316,128 @@ function http(proxy) {
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
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');
return result.toString();
}
function wireguard(proxy) {
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
proxy.server = proxy.peers[0].server;
proxy.port = proxy.peers[0].port;
proxy.ip = proxy.peers[0].ip;
proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-key'];
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
proxy.reserved = proxy.peers[0].reserved;
}
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(`,interface-ip=${proxy.ip}`, 'ip');
result.appendIfPresent(`,interface-ipv6=${proxy.ipv6}`, 'ipv6');
result.appendIfPresent(
`,private-key="${proxy['private-key']}"`,
'private-key',
);
result.appendIfPresent(`,mtu=${proxy.mtu}`, 'mtu');
if (proxy.dns) {
if (Array.isArray(proxy.dns)) {
proxy.dnsv6 = proxy.dns.find((i) => isIPv6(i));
let dns = proxy.dns.find((i) => isIPv4(i));
if (!dns) {
dns = proxy.dns.find((i) => !isIPv4(i) && !isIPv6(i));
}
proxy.dns = dns;
}
}
result.appendIfPresent(`,dns=${proxy.dns}`, 'dns');
result.appendIfPresent(`,dnsv6=${proxy.dnsv6}`, 'dnsv6');
result.appendIfPresent(
`,keepalive=${proxy['persistent-keepalive']}`,
'persistent-keepalive',
);
result.appendIfPresent(`,keepalive=${proxy.keepalive}`, 'keepalive');
const allowedIps = Array.isArray(proxy['allowed-ips'])
? proxy['allowed-ips'].join(',')
: proxy['allowed-ips'];
let reserved = Array.isArray(proxy.reserved)
? proxy.reserved.join(',')
: proxy.reserved;
if (reserved) {
reserved = `,reserved=[${reserved}]`;
}
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
if (presharedKey) {
presharedKey = `,preshared-key="${presharedKey}"`;
}
result.append(
`,peers=[{public-key="${proxy['public-key']}",allowed-ips="${
allowedIps ?? '0.0.0.0/0,::/0'
}",endpoint=${proxy.server}:${proxy.port}${reserved ?? ''}${
presharedKey ?? ''
}}]`,
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=Hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,"${proxy.password}"`, 'password');
// sni
result.appendIfPresent(`,tls-name=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tfo
result.appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
if (proxy.udp) {
result.append(`,udp=true`);
}
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}

View File

@@ -3,7 +3,8 @@ import { isPresent, Result } from './utils';
const targetPlatform = 'QX';
export default function QX_Producer() {
const produce = (proxy) => {
// eslint-disable-next-line no-unused-vars
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
@@ -17,6 +18,8 @@ export default function QX_Producer() {
return http(proxy);
case 'socks5':
return socks5(proxy);
case 'vless':
return vless(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -29,7 +32,36 @@ function shadowsocks(proxy) {
const result = new Result(proxy);
const append = result.append.bind(result);
const appendIfPresent = result.appendIfPresent.bind(result);
if (!proxy.cipher) {
proxy.cipher = 'none';
}
if (
![
'none',
'rc4-md5',
'rc4-md5-6',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'bf-cfb',
'cast5-cfb',
'des-cfb',
'rc2-cfb',
'salsa20',
'chacha20',
'chacha20-ietf',
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
append(`shadowsocks=${proxy.server}:${proxy.port}`);
append(`,method=${proxy.cipher}`);
append(`,password=${proxy.password}`);
@@ -62,18 +94,33 @@ function shadowsocks(proxy) {
);
}
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -81,6 +128,12 @@ function shadowsocks(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
@@ -113,6 +166,12 @@ function shadowsocksr(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
@@ -133,11 +192,11 @@ function trojan(proxy) {
if (needTls(proxy)) append(`,obfs=wss`);
else append(`,obfs=ws`);
appendIfPresent(
`,obfs-uri=${proxy['ws-opts'].path}`,
`,obfs-uri=${proxy['ws-opts']?.path}`,
'ws-opts.path',
);
appendIfPresent(
`,obfs-host=${proxy['ws-opts'].headers.Host}`,
`,obfs-host=${proxy['ws-opts']?.headers?.Host}`,
'ws-opts.headers.Host',
);
} else {
@@ -150,18 +209,33 @@ function trojan(proxy) {
append(`,over-tls=true`);
}
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -169,6 +243,12 @@ function trojan(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
@@ -206,12 +286,18 @@ function vmess(proxy) {
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
appendIfPresent(
`,obfs-uri=${proxy[`${proxy.network}-opts`].path}`,
`,obfs-uri=${
Array.isArray(transportPath) ? transportPath[0] : transportPath
}`,
`${proxy.network}-opts.path`,
);
appendIfPresent(
`,obfs-host=${proxy[`${proxy.network}-opts`].headers.Host}`,
`,obfs-host=${
Array.isArray(transportHost) ? transportHost[0] : transportHost
}`,
`${proxy.network}-opts.headers.Host`,
);
} else {
@@ -219,18 +305,33 @@ function vmess(proxy) {
if (proxy.tls) append(`,obfs=over-tls`);
}
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// AEAD
if (isPresent(proxy, 'aead')) {
@@ -245,6 +346,111 @@ function vmess(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
return result.toString();
}
function vless(proxy) {
if (typeof proxy.flow !== 'undefined' || proxy['reality-opts']) {
throw new Error(`VLESS XTLS/REALITY is not supported`);
}
const result = new Result(proxy);
const append = result.append.bind(result);
const appendIfPresent = result.appendIfPresent.bind(result);
append(`vless=${proxy.server}:${proxy.port}`);
// The method field for vless should be none.
let cipher = 'none';
// if (proxy.cipher === 'auto') {
// cipher = 'chacha20-ietf-poly1305';
// } else {
// cipher = proxy.cipher;
// }
append(`,method=${cipher}`);
append(`,password=${proxy.uuid}`);
// obfs
if (needTls(proxy)) {
proxy.tls = true;
}
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
if (proxy.tls) append(`,obfs=wss`);
else append(`,obfs=ws`);
} else if (proxy.network === 'http') {
append(`,obfs=http`);
} else if (!['tcp'].includes(proxy.network)) {
throw new Error(`network ${proxy.network} is unsupported`);
}
let transportPath = proxy[`${proxy.network}-opts`]?.path;
let transportHost = proxy[`${proxy.network}-opts`]?.headers?.Host;
appendIfPresent(
`,obfs-uri=${
Array.isArray(transportPath) ? transportPath[0] : transportPath
}`,
`${proxy.network}-opts.path`,
);
appendIfPresent(
`,obfs-host=${
Array.isArray(transportHost) ? transportHost[0] : transportHost
}`,
`${proxy.network}-opts.headers.Host`,
);
} else {
// over-tls
if (proxy.tls) append(`,obfs=over-tls`);
}
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
@@ -266,18 +472,33 @@ function http(proxy) {
}
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -285,6 +506,12 @@ function http(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
@@ -306,18 +533,33 @@ function socks5(proxy) {
}
appendIfPresent(`,over-tls=${proxy.tls}`, 'tls');
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
if (needTls(proxy)) {
appendIfPresent(
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
'tls-pubkey-sha256',
);
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
appendIfPresent(
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
'tls-no-session-ticket',
);
appendIfPresent(
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
'tls-no-session-reuse',
);
// tls fingerprint
appendIfPresent(
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
// tls verification
appendIfPresent(
`,tls-verification=${!proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
}
// tfo
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
@@ -325,6 +567,12 @@ function socks5(proxy) {
// udp
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// server_check_url
result.appendIfPresent(
`,server_check_url=${proxy['test-url']}`,
'test-url',
);
// tag
append(`,tag=${proxy.name}`);
@@ -332,11 +580,5 @@ function socks5(proxy) {
}
function needTls(proxy) {
return (
proxy.tls ||
proxy.sni ||
typeof proxy['skip-cert-verify'] !== 'undefined' ||
typeof proxy['tls-fingerprint'] !== 'undefined' ||
typeof proxy['tls-host'] !== 'undefined'
);
return proxy.tls;
}

View File

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

View File

@@ -0,0 +1,818 @@
import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
import { isIPv4, isIPv6 } from '@/utils';
const detourParser = (proxy, parsedProxy) => {
if (proxy['dialer-proxy']) parsedProxy.detour = proxy['dialer-proxy'];
};
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
if (proxy.tfo) parsedProxy.tcp_fast_open = true;
if (proxy.tcp_fast_open) parsedProxy.tcp_fast_open = true;
if (proxy['tcp-fast-open']) parsedProxy.tcp_fast_open = true;
if (!parsedProxy.tcp_fast_open) delete parsedProxy.tcp_fast_open;
};
const smuxParser = (smux, proxy) => {
if (!smux || !smux.enabled) return;
proxy.multiplex = { enabled: true };
proxy.multiplex.protocol = smux.protocol;
if (smux['max-connections'])
proxy.multiplex.max_connections = parseInt(
`${smux['max-connections']}`,
10,
);
if (smux['max-streams'])
proxy.multiplex.max_streams = parseInt(`${smux['max-streams']}`, 10);
if (smux['min-streams'])
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
if (smux.padding) proxy.multiplex.padding = true;
};
const wsParser = (proxy, parsedProxy) => {
const transport = { type: 'ws', headers: {} };
if (proxy['ws-opts']) {
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
if (wsPath !== '') transport.path = `${wsPath}`;
if (Object.keys(wsHeaders).length > 0) {
const headers = {};
for (const key of Object.keys(wsHeaders)) {
let value = wsHeaders[key];
if (value === '') continue;
if (!Array.isArray(value)) value = [`${value}`];
if (value.length > 0) headers[key] = value;
}
const { Host: wsHost } = headers;
if (wsHost.length === 1)
for (const item of `Host:${wsHost[0]}`.split('\n')) {
const [key, value] = item.split(':');
if (value.trim() === '') continue;
headers[key.trim()] = value.trim().split(',');
}
transport.headers = headers;
}
}
if (proxy['ws-headers']) {
const headers = {};
for (const key of Object.keys(proxy['ws-headers'])) {
let value = proxy['ws-headers'][key];
if (value === '') continue;
if (!Array.isArray(value)) value = [`${value}`];
if (value.length > 0) headers[key] = value;
}
const { Host: wsHost } = headers;
if (wsHost.length === 1)
for (const item of `Host:${wsHost[0]}`.split('\n')) {
const [key, value] = item.split(':');
if (value.trim() === '') continue;
headers[key.trim()] = value.trim().split(',');
}
for (const key of Object.keys(headers))
transport.headers[key] = headers[key];
}
if (proxy['ws-path'] && proxy['ws-path'] !== '')
transport.path = `${proxy['ws-path']}`;
if (transport.path) {
const reg = /^(.*?)(?:\?ed=(\d+))?$/;
// eslint-disable-next-line no-unused-vars
const [_, path = '', ed = ''] = reg.exec(transport.path);
transport.path = path;
if (ed !== '') {
transport.early_data_header_name = 'Sec-WebSocket-Protocol';
transport.max_early_data = parseInt(ed, 10);
}
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.headers.Host[0];
if (proxy['ws-opts'] && proxy['ws-opts']['v2ray-http-upgrade']) {
transport.type = 'httpupgrade';
if (transport.headers.Host) {
transport.host = transport.headers.Host[0];
delete transport.headers.Host;
}
if (transport.max_early_data) delete transport.max_early_data;
if (transport.early_data_header_name)
delete transport.early_data_header_name;
}
for (const key of Object.keys(transport.headers)) {
const value = transport.headers[key];
if (value.length === 1) transport.headers[key] = value[0];
}
parsedProxy.transport = transport;
};
const h1Parser = (proxy, parsedProxy) => {
const transport = { type: 'http', headers: {} };
if (proxy['http-opts']) {
const {
method = '',
path: h1Path = '',
headers: h1Headers = {},
} = proxy['http-opts'];
if (method !== '') transport.method = method;
if (Array.isArray(h1Path)) {
transport.path = `${h1Path[0]}`;
} else if (h1Path !== '') transport.path = `${h1Path}`;
for (const key of Object.keys(h1Headers)) {
let value = h1Headers[key];
if (value === '') continue;
if (key.toLowerCase() === 'host') {
let host = value;
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
continue;
}
if (!Array.isArray(value))
value = `${value}`.split(',').map((i) => i.trim());
if (value.length > 0) transport.headers[key] = value;
}
}
if (proxy['http-host'] && proxy['http-host'] !== '') {
let host = proxy['http-host'];
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
if (!transport.host) return;
if (proxy['http-path'] && proxy['http-path'] !== '') {
const path = proxy['http-path'];
if (Array.isArray(path)) {
transport.path = `${path[0]}`;
} else if (path !== '') transport.path = `${path}`;
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host.length === 1) transport.host = transport.host[0];
for (const key of Object.keys(transport.headers)) {
const value = transport.headers[key];
if (value.length === 1) transport.headers[key] = value[0];
}
parsedProxy.transport = transport;
};
const h2Parser = (proxy, parsedProxy) => {
const transport = { type: 'http' };
if (proxy['h2-opts']) {
let { host = '', path = '' } = proxy['h2-opts'];
if (path !== '') transport.path = `${path}`;
if (host !== '') {
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
}
if (proxy['h2-host'] && proxy['h2-host'] !== '') {
let host = proxy['h2-host'];
if (!Array.isArray(host))
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
if (proxy['h2-path'] && proxy['h2-path'] !== '')
transport.path = `${proxy['h2-path']}`;
parsedProxy.tls.enabled = true;
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host.length === 1) transport.host = transport.host[0];
parsedProxy.transport = transport;
};
const grpcParser = (proxy, parsedProxy) => {
const transport = { type: 'grpc' };
if (proxy['grpc-opts']) {
const serviceName = proxy['grpc-opts']['grpc-service-name'];
if (serviceName != null && serviceName !== '')
transport.service_name = `${serviceName}`;
}
parsedProxy.transport = transport;
};
const tlsParser = (proxy, parsedProxy) => {
if (proxy.tls) parsedProxy.tls.enabled = true;
if (proxy.servername && proxy.servername !== '')
parsedProxy.tls.server_name = proxy.servername;
if (proxy.peer && proxy.peer !== '')
parsedProxy.tls.server_name = proxy.peer;
if (proxy.sni && proxy.sni !== '') parsedProxy.tls.server_name = proxy.sni;
if (proxy['skip-cert-verify']) parsedProxy.tls.insecure = true;
if (proxy.insecure) parsedProxy.tls.insecure = true;
if (proxy['disable-sni']) parsedProxy.tls.disable_sni = true;
if (typeof proxy.alpn === 'string') {
parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = proxy.ca_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['reality-opts']) {
parsedProxy.tls.reality = { enabled: true };
if (proxy['reality-opts']['public-key'])
parsedProxy.tls.reality.public_key =
proxy['reality-opts']['public-key'];
if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id'];
}
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
};
const sshParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'ssh',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.user = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
// https://wiki.metacubex.one/config/proxies/ssh
// https://sing-box.sagernet.org/zh/configuration/outbound/ssh
if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
if (proxy['private-key'])
parsedProxy.private_key_path = proxy['private-key'];
if (proxy['private-key-passphrase'])
parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
if (proxy['server-fingerprint']) {
parsedProxy.host_key = [proxy['server-fingerprint']];
// https://manual.nssurge.com/policy/ssh.html
// Surge only supports curve25519-sha256 as the kex algorithm and aes128-gcm as the encryption algorithm. It means that the SSH server must use OpenSSH v7.3 or above. (It should not be a problem since OpenSSH 7.3 was released on 2016-08-01.)
// TODO: ?
parsedProxy.host_key_algorithms = [
proxy['server-fingerprint'].split(' ')[0],
];
}
if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];
if (proxy['host-key-algorithms'])
parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
return parsedProxy;
};
const httpParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'http',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.username = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
if (proxy.headers) {
parsedProxy.headers = {};
for (const k of Object.keys(proxy.headers)) {
parsedProxy.headers[k] = `${proxy.headers[k]}`;
}
if (Object.keys(parsedProxy.headers).length === 0)
delete parsedProxy.headers;
}
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const socks5Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'socks',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
version: '5',
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.username) parsedProxy.username = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password;
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
return parsedProxy;
};
const shadowTLSParser = (proxy = {}) => {
const ssPart = {
tag: proxy.name,
type: 'shadowsocks',
method: proxy.cipher,
password: proxy.password,
detour: `${proxy.name}_shadowtls`,
};
const stPart = {
tag: `${proxy.name}_shadowtls`,
type: 'shadowtls',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
version: proxy['plugin-opts'].version,
password: proxy['plugin-opts'].password,
tls: {
enabled: true,
server_name: proxy['plugin-opts'].host,
utls: {
enabled: true,
fingerprint: proxy['client-fingerprint'],
},
},
};
if (stPart.server_port < 0 || stPart.server_port > 65535)
throw '端口值非法';
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
tfoParser(proxy, stPart);
detourParser(proxy, stPart);
smuxParser(proxy.smux, ssPart);
return { type: 'ss-with-st', ssPart, stPart };
};
const ssParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'shadowsocks',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
method: proxy.cipher,
password: proxy.password,
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
if (proxy.plugin) {
const optArr = [];
if (proxy.plugin === 'obfs') {
parsedProxy.plugin = 'obfs-local';
parsedProxy.plugin_opts = '';
if (proxy['obfs-host'])
proxy['plugin-opts'].host = proxy['obfs-host'];
Object.keys(proxy['plugin-opts']).forEach((k) => {
switch (k) {
case 'mode':
optArr.push(`obfs=${proxy['plugin-opts'].mode}`);
break;
case 'host':
optArr.push(`obfs-host=${proxy['plugin-opts'].host}`);
break;
default:
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
break;
}
});
}
if (proxy.plugin === 'v2ray-plugin') {
parsedProxy.plugin = 'v2ray-plugin';
if (proxy['ws-host']) proxy['plugin-opts'].host = proxy['ws-host'];
if (proxy['ws-path']) proxy['plugin-opts'].path = proxy['ws-path'];
Object.keys(proxy['plugin-opts']).forEach((k) => {
switch (k) {
case 'tls':
if (proxy['plugin-opts'].tls) optArr.push('tls');
break;
case 'host':
optArr.push(`host=${proxy['plugin-opts'].host}`);
break;
case 'path':
optArr.push(`path=${proxy['plugin-opts'].path}`);
break;
case 'headers':
optArr.push(
`headers=${JSON.stringify(
proxy['plugin-opts'].headers,
)}`,
);
break;
case 'mux':
if (proxy['plugin-opts'].mux)
parsedProxy.multiplex = { enabled: true };
break;
default:
optArr.push(`${k}=${proxy['plugin-opts'][k]}`);
}
});
}
parsedProxy.plugin_opts = optArr.join(';');
}
return parsedProxy;
};
// eslint-disable-next-line no-unused-vars
const ssrParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'shadowsocksr',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
method: proxy.cipher,
password: proxy.password,
obfs: proxy.obfs,
protocol: proxy.protocol,
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['obfs-param']) parsedProxy.obfs_param = proxy['obfs-param'];
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
parsedProxy.protocol_param = proxy['protocol-param'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const vmessParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'vmess',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
security: proxy.cipher,
alter_id: parseInt(`${proxy.alterId}`, 10),
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (
[
'auto',
'none',
'zero',
'aes-128-gcm',
'chacha20-poly1305',
'aes-128-ctr',
].indexOf(parsedProxy.security) === -1
)
parsedProxy.security = 'auto';
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const vlessParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'vless',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
tls: { enabled: false, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy);
return parsedProxy;
};
const trojanParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'trojan',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteriaParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
disable_mtu_discovery: false,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.auth_str) parsedProxy.auth_str = `${proxy.auth_str}`;
if (proxy['auth-str']) parsedProxy.auth_str = `${proxy['auth-str']}`;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// eslint-disable-next-line no-control-regex
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
if (reg.test(`${proxy.up}`)) {
parsedProxy.up = `${proxy.up}`;
} else {
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
}
if (reg.test(`${proxy.down}`)) {
parsedProxy.down = `${proxy.down}`;
} else {
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
}
if (proxy.obfs) parsedProxy.obfs = proxy.obfs;
if (proxy.recv_window_conn)
parsedProxy.recv_window_conn = proxy.recv_window_conn;
if (proxy['recv-window-conn'])
parsedProxy.recv_window_conn = proxy['recv-window-conn'];
if (proxy.recv_window) parsedProxy.recv_window = proxy.recv_window;
if (proxy['recv-window']) parsedProxy.recv_window = proxy['recv-window'];
if (proxy.disable_mtu_discovery) {
if (typeof proxy.disable_mtu_discovery === 'boolean') {
parsedProxy.disable_mtu_discovery = proxy.disable_mtu_discovery;
} else {
if (proxy.disable_mtu_discovery === 1)
parsedProxy.disable_mtu_discovery = true;
}
}
tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'hysteria2',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
obfs: {},
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.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;
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const tuic5Parser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'tuic',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
uuid: proxy.uuid,
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (
proxy['congestion-controller'] &&
proxy['congestion-controller'] !== 'cubic'
)
parsedProxy.congestion_control = proxy['congestion-controller'];
if (proxy['udp-relay-mode'] && proxy['udp-relay-mode'] !== 'native')
parsedProxy.udp_relay_mode = proxy['udp-relay-mode'];
if (proxy['reduce-rtt']) parsedProxy.zero_rtt_handshake = true;
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6']
.map((i) => proxy[i])
.map((i) => {
if (isIPv4(i)) return `${i}/32`;
if (isIPv6(i)) return `${i}/128`;
})
.filter((i) => i);
const parsedProxy = {
tag: proxy.name,
type: 'wireguard',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
local_address,
private_key: proxy['private-key'],
peer_public_key: proxy['public-key'],
pre_shared_key: proxy['pre-shared-key'],
reserved: [],
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (typeof proxy.reserved === 'string') {
parsedProxy.reserved = proxy.reserved;
} else if (Array.isArray(proxy.reserved)) {
for (const r of proxy.reserved) parsedProxy.reserved.push(r);
} else {
delete parsedProxy.reserved;
}
if (proxy.peers && proxy.peers.length > 0) {
parsedProxy.peers = [];
for (const p of proxy.peers) {
const peer = {
server: p.server,
server_port: parseInt(`${p.port}`, 10),
public_key: p['public-key'],
allowed_ips: p['allowed-ips'] || p.allowed_ips,
reserved: [],
};
if (typeof p.reserved === 'string') {
peer.reserved.push(p.reserved);
} else if (Array.isArray(p.reserved)) {
for (const r of p.reserved) peer.reserved.push(r);
} else {
delete peer.reserved;
}
if (p['pre-shared-key']) peer.pre_shared_key = p['pre-shared-key'];
parsedProxy.peers.push(peer);
}
}
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
export default function singbox_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
const list = [];
ClashMeta_Producer()
.produce(proxies, 'internal', { 'include-unsupported-proxy': true })
.map((proxy) => {
try {
switch (proxy.type) {
case 'ssh':
list.push(sshParser(proxy));
break;
case 'http':
list.push(httpParser(proxy));
break;
case 'socks5':
if (proxy.tls) {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with tls`,
);
} else {
list.push(socks5Parser(proxy));
}
break;
case 'ss':
// if (!proxy.cipher) {
// proxy.cipher = 'none';
// }
// if (
// ![
// '2022-blake3-aes-128-gcm',
// '2022-blake3-aes-256-gcm',
// '2022-blake3-chacha20-poly1305',
// 'aes-128-cfb',
// 'aes-128-ctr',
// 'aes-128-gcm',
// 'aes-192-cfb',
// 'aes-192-ctr',
// 'aes-192-gcm',
// 'aes-256-cfb',
// 'aes-256-ctr',
// 'aes-256-gcm',
// 'chacha20-ietf',
// 'chacha20-ietf-poly1305',
// 'none',
// 'rc4-md5',
// 'xchacha20',
// 'xchacha20-ietf-poly1305',
// ].includes(proxy.cipher)
// ) {
// throw new Error(
// `cipher ${proxy.cipher} is not supported`,
// );
// }
if (proxy.plugin === 'shadow-tls') {
const { ssPart, stPart } =
shadowTLSParser(proxy);
list.push(ssPart);
list.push(stPart);
} else {
list.push(ssParser(proxy));
}
break;
case 'ssr':
if (opts['include-unsupported-proxy']) {
list.push(ssrParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
);
}
break;
case 'vmess':
if (
!proxy.network ||
['ws', 'grpc', 'h2', 'http'].includes(
proxy.network,
)
) {
list.push(vmessParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with network ${proxy.network}`,
);
}
break;
case 'vless':
if (
!proxy.flow ||
['xtls-rprx-vision'].includes(proxy.flow)
) {
list.push(vlessParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
);
}
break;
case 'trojan':
if (!proxy.flow) {
list.push(trojanParser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type} with flow ${proxy.flow}`,
);
}
break;
case 'hysteria':
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(hysteria2Parser(proxy));
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {
list.push(tuic5Parser(proxy));
} else {
throw new Error(
`Platform sing-box does not support proxy type: TUIC v4`,
);
}
break;
case 'wireguard':
list.push(wireguardParser(proxy));
break;
default:
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,
);
}
} catch (e) {
// console.log(e);
$.error(e.message ?? e);
}
});
return type === 'internal'
? list
: JSON.stringify({ outbounds: list }, null, 2);
};
return { type, produce };
}

View File

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

View File

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

View File

@@ -1,11 +1,20 @@
import { Result, isPresent } from './utils';
import { isNotBlank } from '@/utils';
import { isNotBlank, getIfNotBlank } from '@/utils';
import $ from '@/core/app';
const targetPlatform = 'Surge';
const ipVersions = {
dual: 'dual',
ipv4: 'v4-only',
ipv6: 'v6-only',
'ipv4-prefer': 'prefer-v4',
'ipv6-prefer': 'prefer-v6',
};
export default function Surge_Producer() {
const produce = (proxy) => {
const produce = (proxy, type, opts = {}) => {
proxy.name = proxy.name.replace(/=|,/g, '');
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
@@ -19,6 +28,18 @@ export default function Surge_Producer() {
return socks5(proxy);
case 'snell':
return snell(proxy);
case 'tuic':
return tuic(proxy);
case 'wireguard-surge':
return wireguard_surge(proxy);
case 'hysteria2':
return hysteria2(proxy);
case 'ssh':
return ssh(proxy);
}
if (opts['include-unsupported-proxy'] && proxy.type === 'wireguard') {
return wireguard(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -30,9 +51,52 @@ export default function Surge_Producer() {
function shadowsocks(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
if (!proxy.cipher) {
proxy.cipher = 'none';
}
if (
![
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
'rc4',
'rc4-md5',
'aes-128-cfb',
'aes-192-cfb',
'aes-256-cfb',
'aes-128-ctr',
'aes-192-ctr',
'aes-256-ctr',
'bf-cfb',
'camellia-128-cfb',
'camellia-192-cfb',
'camellia-256-cfb',
'cast5-cfb',
'des-cfb',
'idea-cfb',
'rc2-cfb',
'seed-cfb',
'salsa20',
'chacha20',
'chacha20-ietf',
'none',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
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',
);
// obfs
if (isPresent(proxy, 'plugin')) {
if (proxy.plugin === 'obfs') {
@@ -45,7 +109,7 @@ 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`);
}
}
@@ -55,6 +119,66 @@ function shadowsocks(proxy) {
// 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',
);
// 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',
);
} 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}`);
}
}
}
// 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();
}
@@ -63,6 +187,14 @@ function trojan(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
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',
);
// transport
handleTransport(result, proxy);
@@ -87,6 +219,48 @@ function trojan(proxy) {
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
@@ -95,6 +269,14 @@ function vmess(proxy) {
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
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',
);
// transport
handleTransport(result, proxy);
@@ -127,9 +309,115 @@ function vmess(proxy) {
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function 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');
// 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',
);
result.appendIfPresent(
`,server-fingerprint="${proxy['server-fingerprint']}"`,
'server-fingerprint',
);
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 http(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
@@ -137,6 +425,14 @@ function http(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
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',
);
// tls fingerprint
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
@@ -155,6 +451,48 @@ function http(proxy) {
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
@@ -165,6 +503,14 @@ function socks5(proxy) {
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,${proxy.password}`, 'password');
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',
);
// tls fingerprint
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
@@ -185,6 +531,48 @@ function socks5(proxy) {
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
@@ -194,22 +582,434 @@ function snell(proxy) {
result.appendIfPresent(`,version=${proxy.version}`, 'version');
result.appendIfPresent(`,psk=${proxy.psk}`, 'psk');
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',
);
// obfs
result.appendIfPresent(
`,obfs=${proxy['obfs-opts'].mode}`,
`,obfs=${proxy['obfs-opts']?.mode}`,
'obfs-opts.mode',
);
result.appendIfPresent(
`,obfs-host=${proxy['obfs-opts'].host}`,
`,obfs-host=${proxy['obfs-opts']?.host}`,
'obfs-opts.host',
);
result.appendIfPresent(
`,obfs-uri=${proxy['obfs-opts'].path}`,
`,obfs-uri=${proxy['obfs-opts']?.path}`,
'obfs-opts.path',
);
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// reuse
result.appendIfPresent(`,reuse=${proxy['reuse']}`, 'reuse');
return result.toString();
}
function tuic(proxy) {
const result = new Result(proxy);
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/adapter/outbound/tuic.go#L197
let type = proxy.type;
if (!proxy.token || proxy.token.length === 0) {
type = 'tuic-v5';
}
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
`,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
'alpn',
);
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',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
// tls fingerprint
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}
function wireguard(proxy) {
if (Array.isArray(proxy.peers) && proxy.peers.length > 0) {
proxy.server = proxy.peers[0].server;
proxy.port = proxy.peers[0].port;
proxy.ip = proxy.peers[0].ip;
proxy.ipv6 = proxy.peers[0].ipv6;
proxy['public-key'] = proxy.peers[0]['public-key'];
proxy['preshared-key'] = proxy.peers[0]['pre-shared-key'];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
proxy['allowed-ips'] = proxy.peers[0]['allowed-ips'];
proxy.reserved = proxy.peers[0].reserved;
}
const result = new Result(proxy);
result.append(`# > WireGuard Proxy ${proxy.name}
# ${proxy.name}=wireguard`);
proxy['section-name'] = getIfNotBlank(proxy['section-name'], proxy.name);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.append(`
# > WireGuard Section ${proxy.name}
[WireGuard ${proxy['section-name']}]
private-key = ${proxy['private-key']}`);
result.appendIfPresent(`\nself-ip = ${proxy.ip}`, 'ip');
result.appendIfPresent(`\nself-ip-v6 = ${proxy.ipv6}`, 'ipv6');
if (proxy.dns) {
if (Array.isArray(proxy.dns)) {
proxy.dns = proxy.dns.join(', ');
}
result.append(`\ndns-server = ${proxy.dns}`);
}
result.appendIfPresent(`\nmtu = ${proxy.mtu}`, 'mtu');
if (ip_version === 'prefer-v6') {
result.append(`\nprefer-ipv6 = true`);
}
const allowedIps = Array.isArray(proxy['allowed-ips'])
? proxy['allowed-ips'].join(',')
: proxy['allowed-ips'];
let reserved = Array.isArray(proxy.reserved)
? proxy.reserved.join('/')
: proxy.reserved;
let presharedKey = proxy['preshared-key'] ?? proxy['pre-shared-key'];
if (presharedKey) {
presharedKey = `,preshared-key="${presharedKey}"`;
}
const peer = {
'public-key': proxy['public-key'],
'allowed-ips': allowedIps ? `"${allowedIps}"` : undefined,
endpoint: `${proxy.server}:${proxy.port}`,
keepalive: proxy['persistent-keepalive'] || proxy.keepalive,
'client-id': reserved,
'preshared-key': presharedKey,
};
result.append(
`\npeer = (${Object.keys(peer)
.filter((k) => peer[k] != null)
.map((k) => `${k} = ${peer[k]}`)
.join(', ')})`,
);
return result.toString();
}
function wireguard_surge(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
// 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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function hysteria2(proxy) {
if (proxy.obfs || proxy['obfs-password']) {
throw new Error(`obfs is unsupported`);
}
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
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',
);
// tls verification
result.appendIfPresent(`,sni=${proxy.sni}`, 'sni');
result.appendIfPresent(
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
'skip-cert-verify',
);
result.appendIfPresent(
`,server-cert-fingerprint-sha256=${proxy['tls-fingerprint']}`,
'tls-fingerprint',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
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',
);
// shadow-tls
if (isPresent(proxy, 'shadow-tls-password')) {
result.append(`,shadow-tls-password=${proxy['shadow-tls-password']}`);
result.appendIfPresent(
`,shadow-tls-version=${proxy['shadow-tls-version']}`,
'shadow-tls-version',
);
result.appendIfPresent(
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
}
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
// download-bandwidth
result.appendIfPresent(
`,download-bandwidth=${`${proxy['down']}`.match(/\d+/)?.[0] || 0}`,
'down',
);
result.appendIfPresent(`,ecn=${proxy.ecn}`, 'ecn');
return result.toString();
}
@@ -225,7 +1025,13 @@ function handleTransport(result, proxy) {
if (isPresent(proxy, 'ws-opts.headers')) {
const headers = proxy['ws-opts'].headers;
const value = Object.keys(headers)
.map((k) => `${k}:${headers[k]}`)
.map((k) => {
let v = headers[k];
if (['Host'].includes(k)) {
v = `"${v}"`;
}
return `${k}:${v}`;
})
.join('|');
if (isNotBlank(value)) {
result.append(`,ws-headers=${value}`);

View File

@@ -0,0 +1,104 @@
import { Result } from './utils';
import Surge_Producer from './surge';
import { isIPv4, isIPv6, isPresent } from '@/utils';
import $ from '@/core/app';
const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() {
const produce = (proxy) => {
switch (proxy.type) {
case 'external':
return external(proxy);
case 'ssr':
return shadowsocksr(proxy);
default:
return surge_Producer.produce(proxy);
}
};
return { produce };
}
function external(proxy) {
const result = new Result(proxy);
if (!proxy.exec || !proxy['local-port']) {
throw new Error(`${proxy.type}: exec and local-port are required`);
}
result.append(
`${proxy.name}=external,exec="${proxy.exec}",local-port=${proxy['local-port']}`,
);
if (Array.isArray(proxy.args)) {
proxy.args.map((args) => {
result.append(`,args="${args}"`);
});
}
if (Array.isArray(proxy.addresses)) {
proxy.addresses.map((addresses) => {
result.append(`,addresses=${addresses}`);
});
}
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tfo
if (isPresent(proxy, 'tfo')) {
result.append(`,tfo=${proxy['tfo']}`);
} else if (isPresent(proxy, 'fast-open')) {
result.append(`,tfo=${proxy['fast-open']}`);
}
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
return result.toString();
}
function shadowsocksr(proxy) {
const external_proxy = {
...proxy,
type: 'external',
exec: proxy.exec || '/usr/local/bin/ssr-local',
'local-port': '__SubStoreLocalPort__',
args: [],
addresses: [],
'local-address':
proxy.local_address ?? proxy['local-address'] ?? '127.0.0.1',
};
// https://manual.nssurge.com/policy/external-proxy.html
if (isIP(proxy.server)) {
external_proxy.addresses.push(proxy.server);
} else {
$.log(
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
);
}
for (const [key, value] of Object.entries({
cipher: '-m',
obfs: '-o',
password: '-k',
port: '-p',
protocol: '-O',
'protocol-param': '-G',
server: '-s',
'local-port': '-l',
'local-address': '-b',
})) {
external_proxy.args.push(value);
external_proxy.args.push(external_proxy[key]);
}
return external(external_proxy);
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}

View File

@@ -1,16 +1,32 @@
/* eslint-disable no-case-declarations */
import { Base64 } from 'js-base64';
import { isIPv6 } from '@/utils';
export default function URI_Producer() {
const type = 'SINGLE';
const produce = (proxy) => {
let result = '';
delete proxy.subName;
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
delete proxy.tls;
}
if (proxy.server && isIPv6(proxy.server)) {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) {
case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
proxy.port
}/`;
}${proxy.plugin ? '/' : ''}`;
if (proxy.plugin) {
result += '?plugin=';
const opts = proxy['plugin-opts'];
@@ -35,6 +51,14 @@ export default function URI_Producer() {
);
}
}
if (proxy['udp-over-tcp']) {
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
}
if (proxy.tfo) {
result = `${result}${
proxy.plugin || proxy['udp-over-tcp'] ? '&' : '?'
}tfo=1`;
}
result += `#${encodeURIComponent(proxy.name)}`;
break;
case 'ssr':
@@ -55,27 +79,402 @@ export default function URI_Producer() {
break;
case 'vmess':
// V2RayN URI format
let type = '';
let net = proxy.network || 'tcp';
if (proxy.network === 'http') {
net = 'tcp';
type = 'http';
} else if (
proxy.network === 'ws' &&
proxy['ws-opts']?.['v2ray-http-upgrade']
) {
net = 'httpupgrade';
}
result = {
v: '2',
ps: proxy.name,
add: proxy.server,
port: proxy.port,
id: proxy.uuid,
type: '',
type,
aid: 0,
net: proxy.network || 'tcp',
net,
tls: proxy.tls ? 'tls' : '',
};
if (proxy.tls && proxy.sni) {
result.sni = proxy.sni;
}
// obfs
if (proxy.network === 'ws') {
result.path = proxy['ws-opts'].path || '/';
result.host = proxy['ws-opts'].headers.Host || proxy.server;
if (proxy.network) {
let vmessTransportPath =
proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
'grpc-service-name'
];
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
}
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));
break;
case 'vless':
let security = 'none';
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
pbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
let alpn = '';
if (proxy.alpn) {
alpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
let allowInsecure = '';
if (proxy['skip-cert-verify']) {
allowInsecure = `&allowInsecure=1`;
}
let sni = '';
if (proxy.sni) {
sni = `&sni=${encodeURIComponent(proxy.sni)}`;
}
let fp = '';
if (proxy['client-fingerprint']) {
fp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let flow = '';
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let 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',
)}`;
}
let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
let vlessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vlessTransportPath) {
vlessTransport += `&path=${encodeURIComponent(
Array.isArray(vlessTransportPath)
? vlessTransportPath[0]
: vlessTransportPath,
)}`;
}
if (vlessTransportHost) {
vlessTransport += `&host=${encodeURIComponent(
Array.isArray(vlessTransportHost)
? vlessTransportHost[0]
: vlessTransportHost,
)}`;
}
if (vlessTransportServiceName) {
vlessTransport += `&serviceName=${encodeURIComponent(
vlessTransportServiceName,
)}`;
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?security=${encodeURIComponent(
security,
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'trojan':
let trojanTransport = '';
if (proxy.network) {
let trojanType = proxy.network;
if (
proxy.network === 'ws' &&
proxy['ws-opts']?.['v2ray-http-upgrade']
) {
trojanType = 'httpupgrade';
}
trojanTransport = `&type=${encodeURIComponent(trojanType)}`;
if (['grpc'].includes(proxy.network)) {
let trojanTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
)}`;
}
let trojanTransportPath =
proxy[`${proxy.network}-opts`]?.path;
let trojanTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (trojanTransportPath) {
trojanTransport += `&path=${encodeURIComponent(
Array.isArray(trojanTransportPath)
? trojanTransportPath[0]
: trojanTransportPath,
)}`;
}
if (trojanTransportHost) {
trojanTransport += `&host=${encodeURIComponent(
Array.isArray(trojanTransportHost)
? trojanTransportHost[0]
: trojanTransportHost,
)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port
}#${encodeURIComponent(proxy.name)}`;
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
break;
case 'hysteria2':
let hysteria2params = [];
if (proxy['skip-cert-verify']) {
hysteria2params.push(`insecure=1`);
}
if (proxy.obfs) {
hysteria2params.push(
`obfs=${encodeURIComponent(proxy.obfs)}`,
);
if (proxy['obfs-password']) {
hysteria2params.push(
`obfs-password=${encodeURIComponent(
proxy['obfs-password'],
)}`,
);
}
}
if (proxy.sni) {
hysteria2params.push(
`sni=${encodeURIComponent(proxy.sni)}`,
);
}
if (proxy.ports) {
hysteria2params.push(`mport=${proxy.ports}`);
}
if (proxy['tls-fingerprint']) {
hysteria2params.push(
`pinSHA256=${encodeURIComponent(
proxy['tls-fingerprint'],
)}`,
);
}
if (proxy.tfo) {
hysteria2params.push(`fastopen=1`);
}
result = `hysteria2://${encodeURIComponent(proxy.password)}@${
proxy.server
}:${proxy.port}?${hysteria2params.join(
'&',
)}#${encodeURIComponent(proxy.name)}`;
break;
case 'hysteria':
let hysteriaParams = [];
Object.keys(proxy).forEach((key) => {
if (!['name', 'type', 'server', 'port'].includes(key)) {
const i = key.replace(/-/, '_');
if (['alpn'].includes(key)) {
if (proxy[key]) {
hysteriaParams.push(
`${i}=${encodeURIComponent(
Array.isArray(proxy[key])
? proxy[key][0]
: proxy[key],
)}`,
);
}
} else if (['skip-cert-verify'].includes(key)) {
if (proxy[key]) {
hysteriaParams.push(`insecure=1`);
}
} else if (['tfo', 'fast-open'].includes(key)) {
if (
proxy[key] &&
!hysteriaParams.includes('fastopen=1')
) {
hysteriaParams.push(`fastopen=1`);
}
} else if (['ports'].includes(key)) {
hysteriaParams.push(`mport=${proxy[key]}`);
} else if (['auth-str'].includes(key)) {
hysteriaParams.push(`auth=${proxy[key]}`);
} else if (['up'].includes(key)) {
hysteriaParams.push(`upmbps=${proxy[key]}`);
} else if (['down'].includes(key)) {
hysteriaParams.push(`downmbps=${proxy[key]}`);
} else if (['_obfs'].includes(key)) {
hysteriaParams.push(`obfs=${proxy[key]}`);
} else if (['obfs'].includes(key)) {
hysteriaParams.push(`obfsParam=${proxy[key]}`);
} else if (['sni'].includes(key)) {
hysteriaParams.push(`peer=${proxy[key]}`);
} else if (proxy[key]) {
hysteriaParams.push(
`${i}=${encodeURIComponent(proxy[key])}`,
);
}
}
});
result = `hysteria://${proxy.server}:${
proxy.port
}?${hysteriaParams.join('&')}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {
let tuicParams = [];
Object.keys(proxy).forEach((key) => {
if (
![
'name',
'type',
'uuid',
'password',
'server',
'port',
].includes(key)
) {
const i = key.replace(/-/, '_');
if (['alpn'].includes(key)) {
if (proxy[key]) {
tuicParams.push(
`${i}=${encodeURIComponent(
Array.isArray(proxy[key])
? proxy[key][0]
: proxy[key],
)}`,
);
}
} else if (['skip-cert-verify'].includes(key)) {
if (proxy[key]) {
tuicParams.push(`allow_insecure=1`);
}
} else if (['tfo', 'fast-open'].includes(key)) {
if (
proxy[key] &&
!tuicParams.includes('fast_open=1')
) {
tuicParams.push(`fast_open=1`);
}
} else if (
['disable-sni', 'reduce-rtt'].includes(key) &&
proxy[key]
) {
tuicParams.push(`${i}=1`);
} else if (proxy[key]) {
tuicParams.push(
`${i}=${encodeURIComponent(proxy[key])}`,
);
}
}
});
result = `tuic://${encodeURIComponent(
proxy.uuid,
)}:${encodeURIComponent(proxy.password)}@${proxy.server}:${
proxy.port
}?${tuicParams.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]) {
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

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

View File

@@ -9,7 +9,9 @@ const RULE_TYPES_MAPPING = [
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
[/^PROTOCOL$/, 'PROTOCOL'],
[/^IP-CIDR$/i, 'IP-CIDR'],
[/^(IP-CIDR6|ip6-cidr|IP6-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);
}
@@ -47,7 +48,7 @@ function AllRuleParser() {
}
if (!matched) throw new Error('Invalid rule type: ' + rawType);
} catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
console.log(`Failed to parse line: ${line}\n Reason: ${e}`);
}
}
return result;

View File

@@ -8,7 +8,7 @@ function HTML() {
function ClashProvider() {
const name = 'Clash Provider';
const test = (raw) => raw.indexOf('payload:') === 0;
const test = (raw) => /^payload:/gm.exec(raw).index >= 0;
const parse = (raw) => {
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
};

View File

@@ -1,4 +1,4 @@
import YAML from 'static-js-yaml';
import YAML from '@/utils/yaml';
function QXFilter() {
const type = 'SINGLE';
@@ -10,6 +10,8 @@ function QXFilter() {
'SRC-IP',
'IN-PORT',
'PROTOCOL',
'GEOSITE',
'GEOIP',
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
@@ -29,9 +31,12 @@ 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 (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
output += rule.options ? `,${rule.options[0]}` : '';
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
output +=
rule.options?.length > 0 ? `,${rule.options.join(',')}` : '';
}
return output;
};
@@ -42,8 +47,14 @@ 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
rule.options = rule.options.filter((option) =>
['no-resolve'].includes(option),
);
}
return SurgeRuleSet().func(rule);
};
return { type, func };
@@ -62,8 +73,17 @@ function ClashRuleProvider() {
let output = `${TRANSFORM[rule.type] || rule.type},${
rule.content
}`;
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
output += rule.options ? `,${rule.options[0]}` : '';
if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
if (rule.options) {
// Clash only supports the no-resolve option
rule.options = rule.options.filter((option) =>
['no-resolve'].includes(option),
);
}
output +=
rule.options?.length > 0
? `,${rule.options.join(',')}`
: '';
}
return output;
}),

View File

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

View File

@@ -1,21 +1,75 @@
import { version } from '../../package.json';
import { SETTINGS_KEY, ARTIFACTS_KEY } from '@/constants';
import {
SETTINGS_KEY,
ARTIFACTS_KEY,
SUBS_KEY,
COLLECTIONS_KEY,
} from '@/constants';
import $ from '@/core/app';
import { produceArtifact } from '@/restful/sync';
import { syncToGist } from '@/restful/artifacts';
import { findByName } from '@/utils/database';
!(async function () {
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(
`
@@ -30,39 +84,118 @@ async function doSync() {
const files = {};
try {
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
} else if (artifact.type === 'collection') {
const collection = findByName(allCols, artifact.source);
if (collection && Array.isArray(collection.subscriptions)) {
collection.subscriptions.map((subName) => {
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
});
}
}
}
});
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
try {
await produceArtifact({
type: 'subscription',
name: subName,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
}
}),
);
}
await Promise.all(
allArtifacts.map(async (artifact) => {
if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`);
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
try {
if (artifact.sync && artifact.source) {
$.info(`正在同步云配置:${artifact.name}...`);
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
produceOpts: {
'include-unsupported-proxy':
artifact.includeUnsupportedProxy,
},
});
files[artifact.name] = {
content: output,
};
// if (!output || output.length === 0)
// throw new Error('该配置的结果为空 不进行上传');
files[encodeURIComponent(artifact.name)] = {
content: output,
};
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
);
invalid.push(artifact.name);
}
}),
);
if (invalid.length > 0) {
throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
for (const artifact of allArtifacts) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
let isGitLab;
if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(
files.map((item) => [item.path, item]),
);
}
const raw_url =
files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.notify('🌍 Sub-Store', '全部订阅同步成功!');
} catch (err) {
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${err}`);
$.error(`无法同步订阅配置到 Gist原因${err}`);
} catch (e) {
$.notify('🌍 Sub-Store', '同步订阅失败', `原因:${e.message ?? e}`);
$.error(`无法同步订阅配置到 Gist原因${e}`);
}
}

View File

@@ -2,28 +2,79 @@
import { ProxyUtils } from '@/core/proxy-utils';
import { RuleUtils } from '@/core/rule-utils';
import { version } from '../../package.json';
import download from '@/utils/download';
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Sub-Store -- v${version}
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
let result = '';
let resource = typeof $resource !== 'undefined' ? $resource : '';
let resourceType = typeof $resourceType !== 'undefined' ? $resourceType : '';
let resourceUrl = typeof $resourceUrl !== 'undefined' ? $resourceUrl : '';
const RESOURCE_TYPE = {
PROXY: 1,
RULE: 2,
};
!(async () => {
console.log(
`
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
Sub-Store -- v${version}
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
`,
);
let result = $resource;
let arg;
if (typeof $argument != 'undefined') {
arg = Object.fromEntries(
$argument.split('&').map((item) => item.split('=')),
);
} else {
arg = {};
}
if ($resourceType === RESOURCE_TYPE.PROXY) {
const proxies = ProxyUtils.parse($resource);
result = ProxyUtils.produce(proxies, 'Loon');
} else if ($resourceType === RESOURCE_TYPE.RULE) {
const rules = RuleUtils.parse($resource);
result = RuleUtils.produce(rules, 'Loon');
}
const RESOURCE_TYPE = {
PROXY: 1,
RULE: 2,
};
$done(result);
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 ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取订阅`);
try {
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
let proxies = ProxyUtils.parse(raw);
result = ProxyUtils.produce(proxies, 'Loon');
} 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 ((!result || /^\s*$/.test(result)) && resourceUrl) {
console.log(`解析器: 尝试从 ${resourceUrl} 获取规则`);
try {
let raw = await download(resourceUrl, arg?.ua, arg?.timeout);
let rules = RuleUtils.parse(raw);
result = RuleUtils.produce(rules, 'Loon');
} catch (e) {
console.log(e.message ?? e);
}
}
}
})()
.catch(async (e) => {
console.log('解析器: 出现错误');
console.log(e.message ?? e);
})
.finally(() => {
$done(result || '');
});

View File

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

View File

@@ -19,7 +19,12 @@ export default function register($app) {
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
// RESTful APIs
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
$app.get('/api/artifacts/restore', restoreArtifacts);
$app.route('/api/artifacts')
.get(getAllArtifacts)
.post(createArtifact)
.put(replaceArtifact);
$app.route('/api/artifact/:name')
.get(getArtifact)
@@ -27,11 +32,84 @@ export default function register($app) {
.delete(deleteArtifact);
}
async function restoreArtifacts(_, res) {
$.info('开始恢复远程配置...');
try {
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) {
return Promise.reject('未设置 GitHub Token');
}
const manager = new Gist({
token: gistToken,
key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
});
try {
const gist = await manager.locate();
if (!gist?.files) {
throw new Error(`找不到 Sub-Store Gist 文件列表`);
}
const allArtifacts = $.read(ARTIFACTS_KEY);
const failed = [];
Object.keys(gist.files).map((key) => {
const filename = gist.files[key]?.filename;
if (filename) {
if (encodeURIComponent(filename) !== filename) {
$.error(`文件名 ${filename} 未编码 不保存`);
failed.push(filename);
} else {
const artifact = findByName(allArtifacts, filename);
if (artifact) {
updateByName(allArtifacts, filename, {
...artifact,
url: gist.files[key]?.raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
),
});
} else {
allArtifacts.push({
name: `${filename}`,
url: gist.files[key]?.raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
),
});
}
}
}
});
$.write(allArtifacts, ARTIFACTS_KEY);
} catch (err) {
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
throw err;
}
success(res);
} catch (e) {
$.error(`恢复远程配置失败,原因:${e.message ?? e}`);
failed(
res,
new InternalServerError(
`FAILED_TO_RESTORE_ARTIFACTS`,
`Failed to restore artifacts`,
`Reason: ${e.message ?? e}`,
),
);
}
}
function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
success(res, allArtifacts);
}
function replaceArtifact(req, res) {
const allArtifacts = req.body;
$.write(allArtifacts, ARTIFACTS_KEY);
success(res);
}
async function getArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
@@ -131,7 +209,18 @@ async function deleteArtifact(req, res) {
files[encodeURIComponent(artifact.name)] = {
content: '',
};
await syncToGist(files);
if (encodeURIComponent(artifact.name) !== artifact.name) {
files[artifact.name] = {
content: '',
};
}
// 当别的Sub 删了同步订阅 或 gist里面删了 当前设备没有删除 时 无法删除的bug
try {
await syncToGist(files);
} catch (i) {
$.error(`Function syncToGist: ${name} : ${i}`);
}
}
// delete local cache
deleteByName(allArtifacts, name);
@@ -155,15 +244,34 @@ function validateArtifactName(name) {
}
async function syncToGist(files) {
const { gistToken } = $.read(SETTINGS_KEY);
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) {
return Promise.reject('未设置Gist Token');
return Promise.reject('未设置 GitHub Token');
}
const manager = new Gist({
token: gistToken,
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

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

View File

@@ -1,4 +1,5 @@
import { getPlatformFromHeaders } from '@/utils/platform';
import { getPlatformFromHeaders } 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';
@@ -6,40 +7,185 @@ 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('/download/collection/:name', downloadCollection);
$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 platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
$.info(
`正在下载订阅:${name}\n请求 User-Agent: ${
req.headers['user-agent'] || req.headers['User-Agent']
}`,
);
let {
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
resultFormat,
} = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地订阅: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) {
try {
const output = await produceArtifact({
let output = await produceArtifact({
type: 'subscription',
name,
platform,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
if (sub.source !== 'local') {
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
url = `${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
`订阅 ${name} 获取流量信息时发生错误: ${JSON.stringify(
err,
)}`,
);
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.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,
);
@@ -50,15 +196,15 @@ async function downloadSubscription(req, res) {
$.notify(
`🌍 Sub-Store 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
`🤔 原因:${err.message ?? err}`,
);
$.error(JSON.stringify(err));
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download subscription: ${name}`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}
@@ -76,8 +222,9 @@ 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 platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
@@ -85,14 +232,44 @@ async function downloadCollection(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
$.info(`正在下载组合订阅:${name}`);
$.info(
`正在下载组合订阅:${name}\n请求 User-Agent: ${
req.headers['user-agent'] || req.headers['User-Agent']
}`,
);
let {
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
resultFormat,
} = req.query;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
if (collection) {
try {
const output = await produceArtifact({
let output = await produceArtifact({
type: 'collection',
name,
platform,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
// forward flow header from the first subscription in this collection
@@ -100,15 +277,74 @@ async function downloadCollection(req, res) {
const subnames = collection.subscriptions;
if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') {
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
let url = `${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
`组合订阅 ${name} 中的子订阅 ${
sub.name
} 获取流量信息时发生错误: ${err.message ?? err}`,
);
}
}
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.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,
);
@@ -126,7 +362,7 @@ async function downloadCollection(req, res) {
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download collection: ${name}`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}
@@ -145,3 +381,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);
}

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

@@ -0,0 +1,194 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import {
RequestInvalidError,
ResourceNotFoundError,
InternalServerError,
} from '@/restful/errors';
import { produceArtifact } from '@/restful/sync';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/wholeFile/:name').get(getWholeFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
$app.route('/api/wholeFiles').get(getAllWholeFiles);
}
// file API
function createFile(req, res) {
const file = req.body;
file.name = `${file.name ?? Date.now()}`;
$.info(`正在创建文件:${file.name}`);
const allFiles = $.read(FILES_KEY);
if (findByName(allFiles, file.name)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
req.body.name
? `已存在 name 为 ${file.name} 的文件`
: `无法同时创建相同的文件 可稍后重试`,
),
);
}
allFiles.push(file);
$.write(allFiles, FILES_KEY);
success(res, file, 201);
}
async function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在下载文件:${name}`);
let { url, ua, content, mergeSources, ignoreFailedRemoteFile } = req.query;
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`);
}
if (ua) {
ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`);
}
if (content) {
content = decodeURIComponent(content);
$.info(`指定本地文件: ${content}`);
}
if (mergeSources) {
mergeSources = decodeURIComponent(mergeSources);
$.info(`指定合并来源: ${mergeSources}`);
}
if (ignoreFailedRemoteFile != null && ignoreFailedRemoteFile !== '') {
ignoreFailedRemoteFile = decodeURIComponent(ignoreFailedRemoteFile);
$.info(`指定忽略失败的远程文件: ${ignoreFailedRemoteFile}`);
}
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
try {
const output = await produceArtifact({
type: 'file',
name,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteFile,
});
res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '',
);
} catch (err) {
$.notify(
`🌍 Sub-Store 下载文件失败`,
`❌ 无法下载文件:${name}`,
`🤔 原因:${err.message ?? err}`,
);
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download file: ${name}`,
`Reason: ${err.message ?? err}`,
),
);
}
} else {
$.notify(`🌍 Sub-Store 下载文件失败`, `❌ 未找到文件:${name}`);
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function getWholeFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
success(res, file);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function updateFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let file = req.body;
const allFiles = $.read(FILES_KEY);
const oldFile = findByName(allFiles, name);
if (oldFile) {
const newFile = {
...oldFile,
...file,
};
$.info(`正在更新文件:${name}...`);
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function deleteFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除文件:${name}`);
let allFiles = $.read(FILES_KEY);
deleteByName(allFiles, name);
$.write(allFiles, FILES_KEY);
success(res);
}
function getAllFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(
res, // eslint-disable-next-line no-unused-vars
allFiles.map(({ content, ...rest }) => rest),
);
}
function getAllWholeFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(res, allFiles);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);
success(res);
}

View File

@@ -1,9 +1,14 @@
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 registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings';
@@ -11,10 +16,16 @@ import registerPreviewRoutes from './preview';
import registerSortingRoutes from './sort';
import registerMiscRoutes from './miscs';
import registerNodeInfoRoutes from './node-info';
import registerParserRoutes from './parser';
export default function serve() {
const $app = express({ substore: $ });
let port;
let host;
if ($.env.isNode) {
port = eval('process.env.SUB_STORE_BACKEND_API_PORT') || 3000;
host = eval('process.env.SUB_STORE_BACKEND_API_HOST') || '::';
}
const $app = express({ substore: $, port, host });
// register routes
registerCollectionRoutes($app);
registerSubscriptionRoutes($app);
@@ -23,9 +34,150 @@ export default function serve() {
registerSortingRoutes($app);
registerSettingRoutes($app);
registerArtifactRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app);
registerNodeInfoRoutes($app);
registerMiscRoutes($app);
registerParserRoutes($app);
$app.start();
if ($.env.isNode) {
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON');
if (backend_cron) {
$.info(`[CRON] ${backend_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_cron,
async function () {
try {
$.info(`[CRON] ${backend_cron} started`);
await syncArtifacts();
$.info(`[CRON] ${backend_cron} finished`);
} catch (e) {
$.error(
`[CRON] ${backend_cron} error: ${e.message ?? e}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const path = eval(`require("path")`);
const fs = eval(`require("fs")`);
const data_url = eval('process.env.SUB_STORE_DATA_URL');
const fe_be_path = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const fe_port = eval('process.env.SUB_STORE_FRONTEND_PORT') || 3001;
const fe_host =
eval('process.env.SUB_STORE_FRONTEND_HOST') || host || '::';
const fe_path = eval('process.env.SUB_STORE_FRONTEND_PATH');
const fe_abs_path = path.resolve(
fe_path || path.join(__dirname, 'frontend'),
);
if (fe_path) {
try {
fs.accessSync(path.join(fe_abs_path, 'index.html'));
} catch (e) {
throw new Error(
`[FRONTEND] index.html file not found in ${fe_abs_path}`,
);
}
const express_ = eval(`require("express")`);
const history = eval(`require("connect-history-api-fallback")`);
const { createProxyMiddleware } = eval(
`require("http-proxy-middleware")`,
);
const app = express_();
const staticFileMiddleware = express_.static(fe_path);
let be_api_rewrite = '';
let be_download_rewrite = '';
let be_api = '/api/';
let be_download = '/download/';
if (fe_be_path) {
if (!fe_be_path.startsWith('/')) {
throw new Error(
'SUB_STORE_FRONTEND_BACKEND_PATH should start with /',
);
}
be_api_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_api}`;
be_download_rewrite = `${
fe_be_path === '/' ? '' : fe_be_path
}${be_download}`;
app.use(
be_api_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_api_rewrite)
? path.replace(be_api_rewrite, be_api)
: path;
},
}),
);
app.use(
be_download_rewrite,
createProxyMiddleware({
target: `http://127.0.0.1:${port}`,
changeOrigin: true,
pathRewrite: (path) => {
return path.startsWith(be_download_rewrite)
? path.replace(be_download_rewrite, be_download)
: path;
},
}),
);
}
app.use(staticFileMiddleware);
app.use(
history({
disableDotRule: true,
verbose: false,
}),
);
app.use(staticFileMiddleware);
const listener = app.listen(fe_port, fe_host, () => {
const { address: fe_address, port: fe_port } =
listener.address();
$.info(`[FRONTEND] ${fe_address}:${fe_port}`);
if (fe_be_path) {
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_api_rewrite} -> http://127.0.0.1:${port}${be_api}`,
);
$.info(
`[FRONTEND -> BACKEND] ${fe_address}:${fe_port}${be_download_rewrite} -> http://127.0.0.1:${port}${be_download}`,
);
}
});
}
if (data_url) {
$.info(`[BACKEND] downloading data from ${data_url}`);
download(data_url)
.then((content) => {
$.write(content, '#sub-store');
$.cache = JSON.parse(content);
$.persistCache();
migrate();
$.info(`[BACKEND] restored data from ${data_url}`);
})
.catch((e) => {
$.error(`[BACKEND] restore data failed`);
console.error(e);
throw e;
});
}
}
}

View File

@@ -1,9 +1,10 @@
import $ from '@/core/app';
import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
import { version as substoreVersion } from '../../package.json';
import { updateArtifactStore, updateGitHubAvatar } from '@/restful/settings';
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 +13,7 @@ import {
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist';
import migrate from '@/utils/migration';
import env from '@/utils/env';
export default function register($app) {
// utils
@@ -22,12 +24,26 @@ export default function register($app) {
// Storage management
$app.route('/api/storage')
.get((req, res) => {
res.json($.read('#sub-store'));
res.set('content-type', 'application/json')
.set(
'content-disposition',
'attachment; filename="sub-store.json"',
)
.send(
$.env.isNode
? JSON.stringify($.cache)
: $.read('#sub-store'),
);
})
.post((req, res) => {
const data = req.body;
$.write(JSON.stringify(data), '#sub-store');
res.end();
const { content } = req.body;
$.write(content, '#sub-store');
if ($.env.isNode) {
$.cache = JSON.parse(content);
$.persistCache();
}
migrate();
success(res);
});
// Redirect sub.store to vercel webpage
@@ -49,35 +65,25 @@ export default function register($app) {
}
function getEnv(req, res) {
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
success(res, {
backend,
version: substoreVersion,
});
success(res, env);
}
async function refresh(_, res) {
// 1. get GitHub avatar and artifact store
await updateGitHubAvatar();
await updateAvatar();
await updateArtifactStore();
// 2. clear resource cache
resourceCache.revokeAll();
scriptResourceCache.revokeAll();
headersResourceCache.revokeAll();
success(res);
}
async function gistBackup(req, res) {
const { action } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) {
failed(
res,
@@ -90,6 +96,7 @@ async function gistBackup(req, res) {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
try {
let content;
@@ -118,6 +125,23 @@ async function gistBackup(req, res) {
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) {
@@ -125,18 +149,23 @@ async function gistBackup(req, res) {
$.cache = content;
$.persistCache();
}
// perform migration after restoring from gist
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
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!`,
`Reason: ${JSON.stringify(err)}`,
`Failed to ${action} gist data!`,
`Reason: ${err.message ?? err}`,
),
);
}

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
import { success, failed } from '@/restful/response';
import { ProxyUtils } from '@/core/proxy-utils';
import { RuleUtils } from '@/core/rule-utils';
export default function register($app) {
$app.route('/api/proxy/parse').post(proxy_parser);
$app.route('/api/rule/parse').post(rule_parser);
}
/***
* 感谢 izhangxm 的 PR!
* 目前没有节点操作, 没有支持完整参数, 以后再完善一下
*/
/***
* 代理服务器协议转换接口。
* 请求方法为POST数据为json。需要提供data和client字段。
* data: string, 协议数据每行一个或者是clash
* client: string, 目标平台名称见backend/src/core/proxy-utils/producers/index.js
*
*/
function proxy_parser(req, res) {
const { data, client, content, platform } = req.body;
var result = {};
try {
var proxies = ProxyUtils.parse(data ?? content);
var par_res = ProxyUtils.produce(proxies, client ?? platform);
result['par_res'] = par_res;
} catch (err) {
failed(res, err);
return;
}
success(res, result);
}
/**
* 规则转换接口。
* 请求方法为POST数据为json。需要提供data和client字段。
* data: string, 多行规则字符串
* client: string, 目标平台名称具体见backend/src/core/rule-utils/producers.js
*/
function rule_parser(req, res) {
const { data, client, content, platform } = req.body;
var result = {};
try {
const rules = RuleUtils.parse(data ?? content);
var par_res = RuleUtils.produce(rules, client ?? platform);
result['par_res'] = par_res;
} catch (err) {
failed(res, err);
return;
}
success(res, result);
}

View File

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

View File

@@ -1,5 +1,6 @@
import { SETTINGS_KEY, ARTIFACT_REPOSITORY_KEY } from '@/constants';
import { success } from './response';
import { success, failed } from './response';
import { InternalServerError } from '@/restful/errors';
import $ from '@/core/app';
import Gist from '@/utils/gist';
@@ -10,42 +11,105 @@ export default function register($app) {
}
async function getSettings(req, res) {
const settings = $.read(SETTINGS_KEY);
if (!settings.avatarUrl) await updateGitHubAvatar();
if (!settings.artifactStore) await updateArtifactStore();
success(res, settings);
try {
let settings = $.read(SETTINGS_KEY);
if (!settings) {
settings = {};
$.write(settings, SETTINGS_KEY);
}
if (!settings.avatarUrl) await updateAvatar();
if (!settings.artifactStore) await updateArtifactStore();
success(res, settings);
} catch (e) {
$.error(`Failed to get settings: ${e.message ?? e}`);
failed(
res,
new InternalServerError(
`FAILED_TO_GET_SETTINGS`,
`Failed to get settings`,
`Reason: ${e.message ?? e}`,
),
);
}
}
async function updateSettings(req, res) {
const settings = $.read(SETTINGS_KEY);
const newSettings = {
...settings,
...req.body,
};
$.write(newSettings, SETTINGS_KEY);
await updateGitHubAvatar();
await updateArtifactStore();
success(res, newSettings);
try {
const settings = $.read(SETTINGS_KEY);
const newSettings = {
...settings,
...req.body,
};
$.write(newSettings, SETTINGS_KEY);
await updateAvatar();
await updateArtifactStore();
success(res, newSettings);
} catch (e) {
$.error(`Failed to update settings: ${e.message ?? e}`);
failed(
res,
new InternalServerError(
`FAILED_TO_UPDATE_SETTINGS`,
`Failed to update settings`,
`Reason: ${e.message ?? e}`,
),
);
}
}
export async function updateGitHubAvatar() {
export async function updateAvatar() {
const settings = $.read(SETTINGS_KEY);
const username = settings.githubUser;
const { githubUser: username, syncPlatform } = settings;
if (username) {
try {
const data = await $.http
.get({
url: `https://api.github.com/users/${username}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
},
})
.then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY);
} catch (e) {
$.error('Failed to fetch GitHub avatar for User: ' + username);
if (syncPlatform === 'gitlab') {
try {
const data = await $.http
.get({
url: `https://gitlab.com/api/v4/users?username=${encodeURIComponent(
username,
)}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
},
})
.then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data[0]['avatar_url'].replace(
/(\?|&)s=\d+(&|$)/,
'$1s=160$2',
);
$.write(settings, SETTINGS_KEY);
} catch (err) {
$.error(
`Failed to fetch GitLab avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
}
} else {
try {
const data = await $.http
.get({
url: `https://api.github.com/users/${encodeURIComponent(
username,
)}`,
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
},
})
.then((resp) => JSON.parse(resp.body));
settings.avatarUrl = data['avatar_url'];
$.write(settings, SETTINGS_KEY);
} catch (err) {
$.error(
`Failed to fetch GitHub avatar for User: ${username}. Reason: ${
err.message ?? err
}`,
);
}
}
}
}
@@ -53,21 +117,30 @@ export async function updateGitHubAvatar() {
export async function updateArtifactStore() {
$.log('Updating artifact store');
const settings = $.read(SETTINGS_KEY);
const { githubUser, gistToken } = settings;
if (githubUser && gistToken) {
const { gistToken, syncPlatform } = settings;
if (gistToken) {
const manager = new Gist({
token: gistToken,
key: ARTIFACT_REPOSITORY_KEY,
syncPlatform,
});
try {
const gistId = await manager.locate();
if (gistId !== -1) {
settings.artifactStore = `https://gist.github.com/${githubUser}/${gistId}`;
$.write(settings, SETTINGS_KEY);
const gist = await manager.locate();
const url = gist?.html_url ?? gist?.web_url;
if (url) {
$.log(`找到 Sub-Store Gist: ${url}`);
// 只需要保证 token 是对的, 现在 username 错误只会导致头像错误
settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID';
} else {
$.error(`找不到 Sub-Store Gist`);
settings.artifactStoreStatus = 'NOT FOUND';
}
} catch (err) {
$.error('Failed to fetch artifact store for User: ' + githubUser);
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
settings.artifactStoreStatus = 'ERROR';
}
$.write(settings, SETTINGS_KEY);
}
}

View File

@@ -1,4 +1,9 @@
import { ARTIFACTS_KEY, COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import {
ARTIFACTS_KEY,
COLLECTIONS_KEY,
SUBS_KEY,
FILES_KEY,
} from '@/constants';
import $ from '@/core/app';
import { success } from '@/restful/response';
@@ -6,12 +11,13 @@ export default function register($app) {
$app.post('/api/sort/subs', sortSubs);
$app.post('/api/sort/collections', sortCollections);
$app.post('/api/sort/artifacts', sortArtifacts);
$app.post('/api/sort/files', sortFiles);
}
function sortSubs(req, res) {
const orders = req.body;
const allSubs = $.read(SUBS_KEY);
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allSubs, SUBS_KEY);
success(res, allSubs);
}
@@ -19,7 +25,7 @@ function sortSubs(req, res) {
function sortCollections(req, res) {
const orders = req.body;
const allCols = $.read(COLLECTIONS_KEY);
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allCols, COLLECTIONS_KEY);
success(res, allCols);
}
@@ -27,7 +33,17 @@ function sortCollections(req, res) {
function sortArtifacts(req, res) {
const orders = req.body;
const allArtifacts = $.read(ARTIFACTS_KEY);
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
allArtifacts.sort(
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
);
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts);
}
function sortFiles(req, res) {
const orders = req.body;
const allFiles = $.read(FILES_KEY);
allFiles.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allFiles, FILES_KEY);
success(res, allFiles);
}

View File

@@ -6,7 +6,11 @@ import {
} from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders } from '@/utils/flow';
import {
getFlowHeaders,
parseFlowHeaders,
getRmainingDays,
} from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
@@ -20,7 +24,10 @@ export default function register($app) {
.patch(updateSubscription)
.delete(deleteSubscription);
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
$app.route('/api/subs')
.get(getAllSubscriptions)
.post(createSubscription)
.put(replaceSubscriptions);
}
// subscriptions API
@@ -40,41 +47,98 @@ 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) {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
});
} 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) {
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) {
failed(
res,
new InternalServerError(
new RequestInvalidError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
'N/A',
`Subscription ${name}: noFlow`,
),
);
return;
}
// unit is KB
const upload = Number(flowHeaders.match(/upload=(\d+)/)[1]);
const download = Number(flowHeaders.match(/download=(\d+)/)[1]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/);
const expires = match ? Number(match[1]) : undefined;
success(res, { expires, total, usage: { upload, download } });
if (sub.subUserinfo) {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} else {
const flowHeaders = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (!flowHeaders) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
return;
}
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
}
} catch (err) {
failed(
res,
@@ -89,6 +153,16 @@ async function getFlowInfo(req, res) {
function createSubscription(req, res) {
const sub = req.body;
$.info(`正在创建订阅: ${sub.name}`);
if (/\//.test(sub.name)) {
failed(
res,
new RequestInvalidError(
'INVALID_NAME',
`Subscription ${sub.name} is invalid`,
),
);
return;
}
const allSubs = $.read(SUBS_KEY);
if (findByName(allSubs, sub.name)) {
failed(
@@ -98,6 +172,7 @@ function createSubscription(req, res) {
`Subscription ${sub.name} already exists.`,
),
);
return;
}
allSubs.push(sub);
$.write(allSubs, SUBS_KEY);
@@ -106,11 +181,21 @@ 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(name)}.json"`,
)
.send(JSON.stringify(sub));
} else {
success(res, sub);
}
} else {
failed(
res,
@@ -198,3 +283,9 @@ function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY);
success(res, allSubs);
}
function replaceSubscriptions(req, res) {
const allSubs = req.body;
$.write(allSubs, SUBS_KEY);
success(res);
}

View File

@@ -4,6 +4,7 @@ import {
COLLECTIONS_KEY,
RULES_KEY,
SUBS_KEY,
FILES_KEY,
} from '@/constants';
import { failed, success } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -22,36 +23,147 @@ export default function register($app) {
$app.get('/api/sync/artifact/:name', syncArtifact);
}
async function produceArtifact({ type, name, platform }) {
async function produceArtifact({
type,
name,
platform,
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
ignoreFailedRemoteFile,
produceType,
produceOpts = {},
subscription,
}) {
platform = platform || 'JSON';
// produce Clash node format for ShadowRocket
if (platform === 'ShadowRocket') platform = 'Clash';
if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
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 (sub.source === 'local') {
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
const errors = {};
raw = await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let subIgnoreFailedRemoteSub = sub.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
}
// parse proxies
let proxies = ProxyUtils.parse(raw);
let proxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
proxies.forEach((proxy) => {
proxy.subName = sub.name;
});
// apply processors
proxies = await ProxyUtils.process(
proxies,
sub.process || [],
platform,
{ [sub.name]: sub },
);
if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify(
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
`⚠️ 订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!',
{
'media-url':
@@ -63,13 +175,15 @@ async function produceArtifact({ type, name, platform }) {
exist[proxy.name] = true;
}
// produce
return ProxyUtils.produce(proxies, platform);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') {
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 results = {};
const errors = {};
let processed = 0;
await Promise.all(
@@ -78,18 +192,69 @@ async function produceArtifact({ type, name, platform }) {
try {
$.info(`正在处理子订阅:${sub.name}...`);
let raw;
if (sub.source === 'local') {
if (
sub.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(
sub.mergeSources,
)
) {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
const errors = {};
raw = await await Promise.all(
sub.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
`订阅 ${sub.name} 的远程订阅 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
} else if (sub.mergeSources === 'remoteFirst') {
raw.push(sub.content);
}
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
let currentProxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
currentProxies.forEach((proxy) => {
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
platform,
{ [sub.name]: sub, _collection: collection },
);
results[name] = currentProxies;
processed++;
@@ -100,31 +265,51 @@ async function produceArtifact({ type, name, platform }) {
);
} catch (err) {
processed++;
errors[name] = err;
$.error(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${err},该订阅已被跳过!进度--${
}时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
);
}
}),
);
let collectionIgnoreFailedRemoteSub = collection.ignoreFailedRemoteSub;
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (
!collectionIgnoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
// merge proxies with the original order
let proxies = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name]),
subnames.map((name) => results[name] || []),
);
proxies.forEach((proxy) => {
proxy.collectionName = collection.name;
});
// apply own processors
proxies = await ProxyUtils.process(
proxies,
collection.process || [],
platform,
{ _collection: collection },
);
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点`);
throw new Error(`组合订阅 ${name} 中不含有效节点`);
}
// check duplicate
const exist = {};
@@ -132,7 +317,7 @@ async function produceArtifact({ type, name, platform }) {
if (exist[proxy.name]) {
$.notify(
'🌍 Sub-Store',
'⚠️ 订阅包含重复节点!',
`⚠️ 组合订阅 ${name} 包含重复节点 ${proxy.name}`,
'请仔细检测配置!',
{
'media-url':
@@ -143,10 +328,11 @@ async function produceArtifact({ type, name, platform }) {
}
exist[proxy.name] = true;
}
return ProxyUtils.produce(proxies, platform);
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'rule') {
const allRules = $.read(RULES_KEY);
const rule = findByName(allRules, name);
if (!rule) throw new Error(`找不到规则 ${name}`);
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
@@ -171,92 +357,305 @@ async function produceArtifact({ type, name, platform }) {
]);
// produce output
return RuleUtils.produce(rules, platform);
} else if (type === 'file') {
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`);
let raw;
if (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;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
} else if (mergeSources === 'remoteFirst') {
raw.push(content);
}
} else if (
file.source === 'local' &&
!['localFirst', 'remoteFirst'].includes(file.mergeSources)
) {
raw = file.content;
} else {
const errors = {};
raw = await Promise.all(
file.url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(url, ua || file.ua);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (!fileIgnoreFailedRemoteFile && Object.keys(errors).length > 0) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
let filesContent = files
.filter((i) => i != null && i !== '')
.join('\n');
// apply processors
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent },
file.process,
)
: { $content: filesContent, $files: files };
return processed?.$content ?? '';
}
}
async function syncAllArtifacts(_, res) {
async function syncArtifacts() {
$.info('开始同步所有远程配置...');
const allArtifacts = $.read(ARTIFACTS_KEY);
const files = {};
try {
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
} else if (artifact.type === 'collection') {
const collection = findByName(allCols, artifact.source);
if (collection && Array.isArray(collection.subscriptions)) {
collection.subscriptions.map((subName) => {
const sub = findByName(allSubs, subName);
if (sub && sub.url && !subNames.includes(subName)) {
subNames.push(subName);
}
});
}
}
}
});
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
try {
await produceArtifact({
type: 'subscription',
name: subName,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
}
}),
);
}
await Promise.all(
allArtifacts.map(async (artifact) => {
if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`);
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
try {
if (artifact.sync && artifact.source) {
$.info(`正在同步云配置:${artifact.name}...`);
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
produceOpts: {
'include-unsupported-proxy':
artifact.includeUnsupportedProxy,
},
});
files[artifact.name] = {
content: output,
};
// if (!output || output.length === 0)
// throw new Error('该配置的结果为空 不进行上传');
files[encodeURIComponent(artifact.name)] = {
content: output,
};
}
} catch (e) {
$.error(
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
);
invalid.push(artifact.name);
}
}),
);
if (invalid.length > 0) {
throw new Error(
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
for (const artifact of allArtifacts) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^/]*\/(.*)/,
'/raw/$1',
);
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
let isGitLab;
if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(
files.map((item) => [item.path, item]),
);
}
const raw_url =
files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
}
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('全部订阅同步成功!');
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
throw e;
}
}
async function syncAllArtifacts(_, res) {
$.info('开始同步所有远程配置...');
try {
await syncArtifacts();
success(res);
} catch (err) {
} catch (e) {
$.error(`同步订阅失败,原因:${e.message ?? e}`);
failed(
res,
new InternalServerError(
`FAILED_TO_SYNC_ARTIFACTS`,
`Failed to sync all artifacts`,
`Reason: ${err}`,
`Reason: ${e.message ?? e}`,
),
);
$.info(`同步订阅失败,原因:${err}`);
}
}
async function syncArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`开始同步远程配置 ${name}...`);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
if (!artifact) {
$.error(`找不到远程配置 ${name}`);
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Artifact ${name} does not exist!`,
`找不到远程配置 ${name}`,
),
404,
);
return;
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
});
if (!artifact.source) {
$.error(`远程配置 ${name} 未设置来源`);
failed(
res,
new ResourceNotFoundError(
'RESOURCE_HAS_NO_SOURCE',
`远程配置 ${name} 未设置来源`,
),
404,
);
return;
}
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
try {
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
produceOpts: {
'include-unsupported-proxy': artifact.includeUnsupportedProxy,
},
});
$.info(
`正在上传配置:${artifact.name}\n>>>${JSON.stringify(
artifact,
null,
2,
)}`,
);
// if (!output || output.length === 0)
// throw new Error('该配置的结果为空 不进行上传');
const resp = await syncToGist({
[encodeURIComponent(artifact.name)]: {
content: output,
@@ -264,12 +663,28 @@ async function syncArtifact(req, res) {
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[
encodeURIComponent(artifact.name)
].raw_url.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
let files = body.files;
let isGitLab;
if (Array.isArray(files)) {
isGitLab = true;
files = Object.fromEntries(files.map((item) => [item.path, item]));
}
const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;
const new_url = isGitLab
? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact);
} catch (err) {
$.error(`远程配置 ${artifact.name} 发生错误: ${err.message ?? err}`);
failed(
res,
new InternalServerError(
@@ -281,4 +696,4 @@ async function syncArtifact(req, res) {
}
}
export { produceArtifact };
export { produceArtifact, syncArtifacts };

View File

@@ -1,44 +1,189 @@
import { HTTP } from '@/vendor/open-api';
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import resourceCache from '@/utils/resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
validCheck,
} from '@/utils/flow';
import $ from '@/core/app';
const tasks = new Map();
export default async function download(url, ua) {
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = hex_md5(ua + url);
if (tasks.has(id)) {
export default async function download(
rawUrl,
ua,
timeout,
proxy,
skipCustomCache,
) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined;
if (customCacheKey && !skipCustomCache) {
const cached = $.read(customCacheKey);
if (cached) {
$.info(
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试更新缓存`,
);
download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
);
return cached;
}
}
// 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, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url);
if (!isNode && tasks.has(id)) {
return tasks.get(id);
}
const http = HTTP({
headers: {
'User-Agent': ua,
'User-Agent': userAgent,
...(isStash && proxy
? { 'X-Stash-Selected-Proxy': encodeURIComponent(proxy) }
: {}),
...(isShadowRocket && proxy ? { 'X-Surge-Policy': proxy } : {}),
},
timeout: requestTimeout,
});
const result = new Promise((resolve, reject) => {
// try to find in app cache
const cached = resourceCache.get(id);
if (cached) {
resolve(cached);
} else {
http.get(url)
.then((resp) => {
const body = resp.body;
if (body.replace(/\s/g, '').length === 0)
reject(new Error('远程资源内容为空!'));
else {
resourceCache.set(id, body);
resolve(body);
}
})
.catch(() => {
reject(new Error(`无法下载 URL${url}`));
});
let result;
// try to find in app cache
const cached = resourceCache.get(id);
if (!$arguments?.noCache && cached && !skipCustomCache) {
$.info(`使用缓存: ${url}`);
result = cached;
} else {
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
);
try {
const { body, headers } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
if (headers) {
const flowInfo = getFlowField(headers);
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
}
}
if (body.replace(/\s/g, '').length === 0)
throw new Error(new Error('远程资源内容为空'));
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) {
$.info(
`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`,
);
$.write(body, customCacheKey);
}
}
result = body;
} catch (e) {
if (customCacheKey) {
const cached = $.read(customCacheKey);
if (cached) {
$.info(
`无法下载 URL ${url}: ${
e.message ?? e
}\n使用自定义缓存 ${$arguments?.cacheKey}`,
);
return cached;
}
}
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
}
});
}
tasks.set(id, result);
// 检查订阅有效性
if ($arguments?.validCheck) {
await validCheck(
parseFlowHeaders(
await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy,
),
),
);
}
if (!isNode) {
tasks.set(id, result);
}
return result;
}

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

@@ -0,0 +1,67 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const {
isNode,
isQX,
isLoon,
isSurge,
isStash,
isShadowRocket,
isLanceX,
isEgern,
isGUIforCores,
} = ENV();
let backend = 'Node';
if (isNode) backend = 'Node';
if (isQX) backend = 'QX';
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket';
if (isEgern) backend = 'Egern';
if (isLanceX) backend = 'LanceX';
if (isGUIforCores) backend = 'GUI.for.Cores';
let meta = {};
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,
meta,
};

View File

@@ -1,15 +1,227 @@
import { HTTP } from '@/vendor/open-api';
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
export async function getFlowHeaders(url) {
const http = HTTP();
const { headers } = await http.get({
url,
headers: {
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
},
});
export function getFlowField(headers) {
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
let url = rawUrl;
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if ($arguments?.noFlow) {
return;
}
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url);
let flowInfo;
if (!$arguments?.noCache && cached) {
// $.info(`使用缓存的流量信息: ${url}`);
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}, User-Agent: ${
userAgent || ''
}`,
);
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) : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
},
timeout: requestTimeout,
});
flowInfo = getFlowField(headers);
}
if (flowInfo) {
headersResourceCache.set(url, flowInfo);
}
}
return flowInfo;
}
export function parseFlowHeaders(flowHeaders) {
if (!flowHeaders) return;
// unit is KB
const uploadMatch = flowHeaders.match(
/upload=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(
/download=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const download = Number(downloadMatch[1] + downloadMatch[2]);
const totalMatch = flowHeaders.match(
/total=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const total = Number(totalMatch[1] + totalMatch[2]);
// optional expire timestamp
const expireMatch = flowHeaders.match(
/expire=([-+]?)([0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)/,
);
const expires = expireMatch
? Number(expireMatch[1] + expireMatch[2])
: undefined;
return { expires, total, usage: { upload, download } };
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024 || unitIndex === unitList.length - 1
? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]);
}
export function validCheck(flow) {
if (!flow) {
throw new Error('没有流量信息');
}
if (flow?.expires && flow.expires * 1000 < Date.now()) {
const date = new Date(flow.expires * 1000).toLocaleDateString();
throw new Error(`订阅已过期: ${date}`);
}
if (flow?.total) {
const upload = flow.usage?.upload || 0;
const download = flow.usage?.download || 0;
if (flow.total - upload - download < 0) {
const current = upload + download;
const currT = flowTransfer(Math.abs(current));
currT.value = current < 0 ? '-' + currT.value : currT.value;
const totalT = flowTransfer(flow.total);
throw new Error(
`流量已用完: ${currT.value} ${currT.unit} / ${totalT.value} ${totalT.unit}`,
);
}
}
}
export function getRmainingDays(opt = {}) {
try {
let { resetDay, startDate, cycleDays } = opt;
if (['string', 'number'].includes(typeof opt)) {
resetDay = opt;
}
if (startDate && cycleDays) {
cycleDays = parseInt(cycleDays);
if (isNaN(cycleDays) || cycleDays <= 0)
throw new Error('重置周期应为正整数');
if (!startDate || !Date.parse(startDate))
throw new Error('开始日期不合法');
const start = new Date(startDate);
const today = new Date();
start.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
if (start.getTime() > today.getTime())
throw new Error('开始日期应早于现在');
let resetDate = new Date(startDate);
resetDate.setDate(resetDate.getDate() + cycleDays);
while (resetDate < today) {
resetDate.setDate(resetDate.getDate() + cycleDays);
}
resetDate.setHours(0, 0, 0, 0);
const timeDiff = resetDate.getTime() - today.getTime();
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
return daysDiff;
} else {
if (!resetDay) return;
resetDay = parseInt(resetDay);
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
throw new Error('月重置日应为 1-31 之间的整数');
let now = new Date();
let today = now.getDate();
let month = now.getMonth();
let year = now.getFullYear();
let daysInMonth;
if (resetDay > today) {
daysInMonth = 0;
} else {
daysInMonth = new Date(year, month + 1, 0).getDate();
}
return daysInMonth - today + resetDay;
}
} catch (e) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}

View File

@@ -1,3 +1,107 @@
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'],
'🇧🇷': ['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', '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'],
'🇱🇹': ['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', 'AMS'],
'🇳🇴': ['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', '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
@@ -65,6 +169,7 @@ export function getFlag(name) {
'广德',
'法兰克福',
'Frankfurt',
'德意志',
],
'🇩🇰': ['Denmark', '丹麦', '丹麥'],
'🇪🇨': ['Ecuador', '厄瓜多尔'],
@@ -155,7 +260,7 @@ export function getFlag(name) {
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
'🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'],
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'],
@@ -213,11 +318,16 @@ export function getFlag(name) {
'🇹🇼': [
'Taiwan',
'台湾',
'臺灣',
'台灣',
'中華民國',
'中华民国',
'台北',
'台中',
'新北',
'彰化',
'台',
'臺',
'Taipei',
],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
@@ -278,108 +388,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'],
'🇺🇦': ['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] ||
@@ -393,7 +401,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);
}
}
@@ -411,6 +421,39 @@ export function getFlag(name) {
return (Flag = flag);
}
}
//console.log(`Final Flag = ${Flag}`)
return Flag;
}
export function getISO(name) {
return ISOFlags[getFlag(name)]?.[0];
}
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');
if (countryFile) {
this.countryReader = Reader.openBuffer(
fs.readFileSync(countryFile),
);
}
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;
}
}

View File

@@ -4,77 +4,233 @@ import { HTTP } from '@/vendor/open-api';
* Gist backup
*/
export default class Gist {
constructor({ token, key }) {
this.http = HTTP({
baseURL: 'https://api.github.com',
headers: {
constructor({ token, key, syncPlatform }) {
if (syncPlatform === 'gitlab') {
this.headers = {
'PRIVATE-TOKEN': `${token}`,
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
};
this.http = HTTP({
baseURL: 'https://gitlab.com/api/v4',
headers: { ...this.headers },
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
const body = JSON.parse(resp.body);
return Promise.reject(
`ERROR: ${body.message?.error ?? body.message}`,
);
} else {
return resp;
}
},
},
});
} else {
this.headers = {
Authorization: `token ${token}`,
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
},
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
return Promise.reject(
`ERROR: ${JSON.parse(resp.body).message}`,
);
} else {
return resp;
}
};
this.http = HTTP({
baseURL: 'https://api.github.com',
headers: { ...this.headers },
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
return Promise.reject(
`ERROR: ${JSON.parse(resp.body).message}`,
);
} else {
return resp;
}
},
},
},
});
});
}
this.key = key;
this.syncPlatform = syncPlatform;
}
async locate() {
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.id;
if (this.syncPlatform === 'gitlab') {
return this.http.get('/snippets').then((response) => {
const gists = JSON.parse(response.body);
for (let g of gists) {
if (g.title === this.key) {
return g;
}
}
}
return -1;
});
}
async upload(files) {
const id = await this.locate();
if (id === -1) {
// create a new gist for backup
return this.http.post({
url: '/gists',
body: JSON.stringify({
description: this.key,
public: false,
files,
}),
return;
});
} else {
// update an existing gist
return this.http.patch({
url: `/gists/${id}`,
body: JSON.stringify({ files }),
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;
});
}
}
async download(filename) {
const id = await this.locate();
if (id === -1) {
return Promise.reject('未找到Gist备份');
async upload(input) {
if (Object.keys(input).length === 0) {
return Promise.reject('未提供需上传的文件');
}
const gist = await this.locate();
let files = input;
if (gist?.id) {
if (this.syncPlatform === 'gitlab') {
gist.files = gist.files.reduce((acc, item) => {
acc[item.path] = item;
return acc;
}, {});
}
// console.log(`files`, files);
// console.log(`gist`, gist.files);
let actions = [];
const result = { ...gist.files };
Object.keys(files).map((key) => {
if (result[key]) {
if (
files[key].content == null ||
files[key].content === ''
) {
delete result[key];
actions.push({
action: 'delete',
file_path: key,
});
} else {
result[key] = files[key];
actions.push({
action: 'update',
file_path: key,
content: files[key].content,
});
}
} else {
if (
files[key].content == null ||
files[key].content === ''
) {
delete result[key];
delete files[key];
} else {
result[key] = files[key];
actions.push({
action: 'create',
file_path: key,
content: files[key].content,
});
}
}
});
// console.log(`result`, result);
// console.log(`files`, files);
// console.log(`actions`, actions);
if (this.syncPlatform === 'gitlab') {
if (Object.keys(result).length === 0) {
return Promise.reject(
'本次操作将导致所有文件的内容都为空, 无法更新 snippet',
);
}
if (Object.keys(result).length > 10) {
return Promise.reject(
'本次操作将导致 snippet 的文件数超过 10, 无法更新 snippet',
);
}
files = actions;
return this.http.put({
headers: {
...this.headers,
'Content-Type': 'application/json',
},
url: `/snippets/${gist.id}`,
body: JSON.stringify({ files }),
});
} else {
if (Object.keys(result).length === 0) {
return Promise.reject(
'本次操作将导致所有文件的内容都为空, 无法更新 gist',
);
}
return this.http.patch({
url: `/gists/${gist.id}`,
body: JSON.stringify({ files }),
});
}
} else {
files = Object.entries(files).reduce((acc, [key, file]) => {
if (file.content !== null && file.content !== '') {
acc[key] = file;
}
return acc;
}, {});
if (this.syncPlatform === 'gitlab') {
if (Object.keys(files).length === 0) {
return Promise.reject(
'所有文件的内容都为空, 无法创建 snippet',
);
}
files = Object.keys(files).map((key) => ({
file_path: key,
content: files[key].content,
}));
return this.http.post({
headers: {
...this.headers,
'Content-Type': 'application/json',
},
url: '/snippets',
body: JSON.stringify({
title: this.key,
visibility: 'private',
files,
}),
});
} else {
if (Object.keys(files).length === 0) {
return Promise.reject(
'所有文件的内容都为空, 无法创建 gist',
);
}
return this.http.post({
url: '/gists',
body: JSON.stringify({
description: this.key,
public: false,
files,
}),
});
}
}
}
async download(filename) {
const gist = await this.locate();
if (gist?.id) {
try {
const { files } = await this.http
.get(`/gists/${id}`)
.get(`/gists/${gist.id}`)
.then((resp) => JSON.parse(resp.body));
const url = files[filename].raw_url;
return await this.http.get(url).then((resp) => resp.body);
} catch (err) {
return Promise.reject(err);
}
} else {
return Promise.reject('找不到 Sub-Store Gist');
}
}
}

View File

@@ -0,0 +1,117 @@
import $ from '@/core/app';
import {
HEADERS_RESOURCE_CACHE_KEY,
CHR_EXPIRATION_TIME_KEY,
} from '@/constants';
class ResourceCache {
constructor() {
this.expires = getExpiredTime();
if (!$.read(HEADERS_RESOURCE_CACHE_KEY)) {
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
}
try {
this.resourceCache = JSON.parse($.read(HEADERS_RESOURCE_CACHE_KEY));
} catch (e) {
$.error(
`解析持久化缓存中的 ${HEADERS_RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
e?.message ?? e
}`,
);
this.resourceCache = {};
$.write('{}', HEADERS_RESOURCE_CACHE_KEY);
}
this._cleanup();
}
_cleanup() {
// clear obsolete cached resource
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
});
if (clear) this._persist();
}
revokeAll() {
this.resourceCache = {};
this._persist();
}
_persist() {
$.write(JSON.stringify(this.resourceCache), HEADERS_RESOURCE_CACHE_KEY);
}
get(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].data;
}
return null;
}
gettime(id) {
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return this.resourceCache[id].time;
}
return null;
}
set(id, value) {
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
}
}
function getExpiredTime() {
// console.log($.read(CHR_EXPIRATION_TIME_KEY));
if (!$.read(CHR_EXPIRATION_TIME_KEY)) {
$.write('6e4', CHR_EXPIRATION_TIME_KEY); // 1分钟
}
let expiration = 6e4;
if ($.env.isLoon) {
const loont = {
// Loon 插件自义定
'1\u5206\u949f': 6e4,
'5\u5206\u949f': 3e5,
'10\u5206\u949f': 6e5,
'30\u5206\u949f': 18e5, // "30分钟"
'1\u5c0f\u65f6': 36e5,
'2\u5c0f\u65f6': 72e5,
'3\u5c0f\u65f6': 108e5,
'6\u5c0f\u65f6': 216e5,
'12\u5c0f\u65f6': 432e5,
'24\u5c0f\u65f6': 864e5,
'48\u5c0f\u65f6': 1728e5,
'72\u5c0f\u65f6': 2592e5, // "72小时"
'\u53c2\u6570\u4f20\u5165': 'readcachets', // "参数输入"
};
let intimed = $.read(
'#\u54cd\u5e94\u5934\u7f13\u5b58\u6709\u6548\u671f',
); // Loon #响应头缓存有效期
// console.log(intimed);
if (intimed in loont) {
expiration = loont[intimed];
if (expiration === 'readcachets') {
expiration = intimed;
}
}
return expiration;
} else {
expiration = $.read(CHR_EXPIRATION_TIME_KEY);
return expiration;
}
}
export default new ResourceCache();

View File

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

View File

@@ -1,23 +0,0 @@
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
break;
}
}
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} 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 {
return 'JSON';
}
}

View File

@@ -7,7 +7,17 @@ class ResourceCache {
if (!$.read(RESOURCE_CACHE_KEY)) {
$.write('{}', RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
try {
this.resourceCache = JSON.parse($.read(RESOURCE_CACHE_KEY));
} catch (e) {
$.error(
`解析持久化缓存中的 ${RESOURCE_CACHE_KEY} 失败, 重置为 {}, 错误: ${
e?.message ?? e
}`,
);
this.resourceCache = {};
$.write('{}', RESOURCE_CACHE_KEY);
}
this._cleanup();
}
@@ -16,8 +26,13 @@ class ResourceCache {
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (new Date().getTime() - updated > this.expires) {
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
@@ -26,9 +41,6 @@ class ResourceCache {
}
revokeAll() {
Object.keys(this.resourceCache).forEach((id) => {
$.delete(`#${id}`);
});
this.resourceCache = {};
this._persist();
}
@@ -38,17 +50,16 @@ class ResourceCache {
}
get(id) {
const updated = this.resourceCache[id];
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return $.read(`#${id}`);
return this.resourceCache[id].data;
}
return null;
}
set(id, value) {
this.resourceCache[id] = new Date().getTime();
this.resourceCache[id] = { time: new Date().getTime(), data: value };
this._persist();
$.write(value, `#${id}`);
}
}

View File

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

View File

@@ -0,0 +1,47 @@
export function getUserAgentFromHeaders(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;
}
}
return { UA, ua };
}
export function getPlatformFromUserAgent({ ua, UA }) {
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';
}
}
export function getPlatformFromHeaders(headers) {
const { UA, ua } = getUserAgentFromHeaders(headers);
return getPlatformFromUserAgent({ ua, UA });
}

37
backend/src/utils/yaml.js Normal file
View File

@@ -0,0 +1,37 @@
import YAML from 'static-js-yaml';
function retry(fn, content, ...args) {
try {
return fn(content, ...args);
} catch (e) {
return fn(
dump(
fn(
content.replace(/!<str>\s*/g, '__SubStoreJSYAMLString__'),
...args,
),
).replace(/__SubStoreJSYAMLString__/g, ''),
...args,
);
}
}
export function safeLoad(content, ...args) {
return retry(YAML.safeLoad, content, ...args);
}
export function load(content, ...args) {
return retry(YAML.load, content, ...args);
}
export function safeDump(...args) {
return YAML.safeDump(...args);
}
export function dump(...args) {
return YAML.dump(...args);
}
export default {
safeLoad,
load,
safeDump,
dump,
};

View File

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

View File

@@ -6,6 +6,9 @@ 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';
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
@@ -46,30 +49,38 @@ 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
let fpath = 'root.json';
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
let rootPath = `${basePath}/root.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx',
});
this.root = {};
} else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
}
// create a json file with the given name if not exists
fpath = `${this.name}.json`;
let fpath = `${basePath}/${this.name}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx',
});
this.cache = {};
} else {
this.cache = JSON.parse(
this.node.fs.readFileSync(`${this.name}.json`),
);
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
}
}
}
@@ -79,15 +90,19 @@ 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') || '.';
this.node.fs.writeFileSync(
`${this.name}.json`,
`${basePath}/${this.name}.json`,
data,
{ flag: 'w' },
(err) => console.log(err),
);
this.node.fs.writeFileSync(
'root.json',
`${basePath}/root.json`,
JSON.stringify(this.root, null, 2),
{ flag: 'w' },
(err) => console.log(err),
@@ -108,6 +123,9 @@ export class OpenAPI {
if (isNode) {
this.root[key] = data;
}
if (isGUIforCores) {
return $Plugins.SubStoreCache.set(key, data);
}
} else {
this.cache[key] = data;
}
@@ -127,6 +145,9 @@ export class OpenAPI {
if (isNode) {
return this.root[key];
}
if (isGUIforCores) {
return $Plugins.SubStoreCache.get(key);
}
} else {
return this.cache[key];
}
@@ -145,6 +166,9 @@ export class OpenAPI {
if (isNode) {
delete this.root[key];
}
if (isGUIforCores) {
return $Plugins.SubStoreCache.remove(key);
}
} else {
delete this.cache[key];
}
@@ -183,6 +207,35 @@ export class OpenAPI {
(openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
if (push) {
const url = push
.replace(
'[推送标题]',
encodeURIComponent(title || 'Sub-Store'),
)
.replace(
'[推送内容]',
encodeURIComponent(
[subtitle, content_].map((i) => i).join('\n'),
),
);
const $http = HTTP();
$http
.get({ url })
.then((resp) => {
console.log(
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
);
})
.catch((e) => {
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
});
}
}
if (isGUIforCores) {
$Plugins.Notify(title, subtitle + '\n' + content);
}
}
@@ -204,7 +257,7 @@ export class OpenAPI {
}
done(value = {}) {
if (isQX || isLoon || isSurge) {
if (isQX || isLoon || isSurge || isGUIforCores) {
$done(value);
} else if (isNode) {
if (typeof $context !== 'undefined') {
@@ -217,11 +270,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',
@@ -271,31 +334,71 @@ 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()](
options,
(err, response, body) => {
if (err) reject(err);
else
resolve({
statusCode:
response.status || response.statusCode,
headers: response.headers,
body,
});
},
);
const opts = JSON.parse(JSON.stringify(options));
if (!isNode && opts.timeout) {
opts.timeout++;
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}`);
}
request[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,
});
});
});
} 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,
});
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(
@@ -308,7 +411,9 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
return (
timer
? Promise.race([timer, worker]).then((res) => {
clearTimeout(timeoutid);
if (typeof clearTimeout !== 'undefined') {
clearTimeout(timeoutid);
}
return res;
})
: worker

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,20 @@
#!name=Sub-Store
#!desc=高级订阅管理工具
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'
#!openUrl=https://sub.store
#!author=Peng-YM
#!homepage=https://github.com/Peng-YM/Sub-Store
#!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
#!select = 响应头缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入
[Rule]
DOMAIN,sub-store.vercel.app,PROXY
[MITM]
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 script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "0 0 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync

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

@@ -0,0 +1,7 @@
{
"name": "Sub-Store",
"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"
]
}

View File

@@ -1,20 +1,39 @@
# Sub-Store 配置指南
## 查看更新说明:
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置:
### 1. Loon
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Loon.plugin)即可。
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
### 2. Surge
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
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)
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
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可
定时任务: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX-Task.json)
### 4. Stash
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/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) 即可。
## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。

View File

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

View File

@@ -0,0 +1,17 @@
#!name=Sub-Store(β)
#!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={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

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

View File

@@ -0,0 +1,12 @@
#!name=Sub-Store
#!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,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

View File

@@ -1,10 +1,17 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM
#!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
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://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
Sub-Store Sync=type=cron,cronexp=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

40
package-lock.json generated
View File

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

217
scripts/demo.js Normal file
View File

@@ -0,0 +1,217 @@
function operator(proxies = [], targetPlatform, context) {
// 支持快捷操作 不一定要写一个 function
// 可参考 https://t.me/zhetengsha/970
// https://t.me/zhetengsha/1009
// proxies 为传入的内部节点数组
// 结构大致参考了 Clash.Meta(mihomo) 有私货
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
// 1. `no-resolve` 为不解析域名
// 2. 域名解析后 会多一个 `resolved` 字段
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// $arguments 为传入的脚本参数
// 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(id, data)
// cache.get(id)
// ProxyUtils 为节点处理工具
// 可参考 https://t.me/zhetengsha/1066
// const ProxyUtils = {
// parse, // 订阅解析
// process, // 节点操作/文件操作
// produce, // 输出订阅
// isIPv4,
// isIPv6,
// isIP,
// yaml, // yaml 解析和生成
// getFlag, // 获取 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类
// }
// 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 从 sni 文件中读取内容并进行节点操作
// const sni = await produceArtifact({
// type: 'file',
// name: 'sni' // 文件名
// });
// $server.sni = sni
// 1. Surge 输出 WireGuard 完整配置
// let proxies = await produceArtifact({
// type: 'subscription',
// name: 'sub',
// platform: 'Surge',
// produceOpts: {
// 'include-unsupported-proxy': true,
// }
// })
// $content = proxies
// 2. sing-box
// 但是一般不需要这样用, 可参考
// 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'
// name: 'sub', // subscription name
// platform: 'sing-box', // target platform
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( JSON.parse('JSON String') )
// })
// // JSON
// $content = JSON.stringify({}, null, 2)
// 3. clash.meta
// 但是一般不需要这样用, 可参考
// 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 is the final content of the file
// flowUtils 为机场订阅流量信息处理工具
// 可参考:
// 1. https://t.me/zhetengsha/948
// context 为传入的上下文
// 有三种情况, 按需判断
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上
// 若存在 `source._collection` 但 `source._collection.subscriptions` 中的 key 在 `source` 上不存在, 说明输出结果为组合订阅, 脚本设置在组合订阅上
// 若不存在 `source._collection`, 说明输出结果为单条订阅, 脚本设置在此单条订阅上
// 1. 输出单条订阅 sub-1 时, 该单条订阅中的脚本上下文为:
// {
// "source": {
// "sub-1": {
// "name": "sub-1",
// "displayName": "",
// "mergeSources": "",
// "ignoreFailedRemoteSub": true,
// "process": [],
// "icon": "",
// "source": "local",
// "url": "",
// "content": "",
// "ua": "",
// "display-name": "",
// "useCacheForFailedRemoteSub": false
// }
// },
// "backend": "Node",
// "version": "2.14.198"
// }
// 2. 输出组合订阅 collection-1 时, 该组合订阅中的脚本上下文为:
// {
// "source": {
// "_collection": {
// "name": "collection-1",
// "displayName": "",
// "mergeSources": "",
// "ignoreFailedRemoteSub": false,
// "icon": "",
// "process": [],
// "subscriptions": [
// "sub-1"
// ],
// "display-name": ""
// }
// },
// "backend": "Node",
// "version": "2.14.198"
// }
// 3. 输出组合订阅 collection-1 时, 该组合订阅中的单条订阅 sub-1 中的某个脚本上下文为:
// {
// "source": {
// "sub-1": {
// "name": "sub-1",
// "displayName": "",
// "mergeSources": "",
// "ignoreFailedRemoteSub": true,
// "icon": "",
// "process": [],
// "source": "local",
// "url": "",
// "content": "",
// "ua": "",
// "display-name": "",
// "useCacheForFailedRemoteSub": false
// },
// "_collection": {
// "name": "collection-1",
// "displayName": "",
// "mergeSources": "",
// "ignoreFailedRemoteSub": false,
// "icon": "",
// "process": [],
// "subscriptions": [
// "sub-1"
// ],
// "display-name": ""
// }
// },
// "backend": "Node",
// "version": "2.14.198"
// }
// 参数说明
// 可参考 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))
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));
}

View File

@@ -17,8 +17,13 @@ class ResourceCache {
let clear = false;
Object.entries(this.resourceCache).forEach((entry) => {
const [id, updated] = entry;
if (new Date().getTime() - updated > this.expires) {
if (!updated.time) {
// clear old version cache
delete this.resourceCache[id];
$.delete(`#${id}`);
clear = true;
}
if (new Date().getTime() - updated.time > this.expires) {
delete this.resourceCache[id];
clear = true;
}
@@ -27,9 +32,6 @@ class ResourceCache {
}
revokeAll() {
Object.keys(this.resourceCache).forEach((id) => {
$.delete(`#${id}`);
});
this.resourceCache = {};
this._persist();
}
@@ -39,17 +41,16 @@ class ResourceCache {
}
get(id) {
const updated = this.resourceCache[id];
const updated = this.resourceCache[id] && this.resourceCache[id].time;
if (updated && new Date().getTime() - updated <= this.expires) {
return $.read(`#${id}`);
return this.resourceCache[id].data;
}
return null;
}
set(id, value) {
this.resourceCache[id] = new Date().getTime();
this.resourceCache[id] = { time: new Date().getTime(), data: value }
this._persist();
$.write(value, `#${id}`);
}
}

1
web

Submodule web deleted from b10b708c34