Compare commits

...

312 Commits

Author SHA1 Message Date
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
K
20d958d74f Update Surge.sgmodule 2023-07-26 01:48:47 +08:00
86 changed files with 10149 additions and 1961 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,7 +17,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'master'
ref: "master"
- name: Set up Node.js
uses: actions/setup-node@v3
with:
@@ -34,21 +34,44 @@ 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: Release
uses: softprops/action-gh-release@v1
if: ${{ success() }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
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.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)
[!["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)
- [x] Clash Proxies YAML
- [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, WireGuard, VLESS, Hysteria 2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] 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.14.2",
"version": "2.14.263",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
@@ -8,14 +8,20 @@
"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"
},
"author": "Peng-YM",
"license": "GPL-3.0",
"dependencies": {
"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",
@@ -36,6 +42,7 @@
"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",

2526
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +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,20 @@
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';
function preprocess(raw) {
for (const processor of PROXY_PREPROCESSORS) {
@@ -36,7 +46,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 +56,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 +69,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;
@@ -114,6 +170,7 @@ async function process(proxies, operators = [], targetPlatform) {
script,
targetPlatform,
$arguments,
source,
);
} else {
processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -123,7 +180,7 @@ 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!`);
@@ -135,12 +192,30 @@ function produce(proxies, targetPlatform) {
!(proxy.supported && proxy.supported[targetPlatform] === false),
);
proxies = proxies.map((proxy) => {
if (!isNotBlank(proxy.name)) {
proxy.name = `${proxy.type} ${proxy.server}:${proxy.port}`;
}
return proxy;
});
$.info(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
return proxies
let localPort = 10000;
const 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 +227,21 @@ function produce(proxies, targetPlatform) {
return '';
}
})
.filter((line) => line.length > 0)
.join('\n');
.filter((line) => line.length > 0);
return type === 'internal' ? list : list.join('\n');
} 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,
};
function tryParse(parser, line) {
@@ -182,3 +261,144 @@ 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') {
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}`;
}
}
return proxy;
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}

View File

@@ -1,4 +1,11 @@
import { getIfNotBlank, isPresent, isNotBlank, getIfPresent } from '@/utils';
import {
isIPv4,
isIPv6,
getIfNotBlank,
isPresent,
isNotBlank,
getIfPresent,
} from '@/utils';
import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx';
@@ -23,12 +30,40 @@ function URI_SS() {
};
content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6
const serverAndPort = content.match(/@([^/]*)(\/|$)/)[1];
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]);
let query = '';
if (!serverAndPortArray) {
if (content.includes('?')) {
const parsed = content.match(/^(.*)(\?.*)$/);
content = parsed[1];
query = parsed[2];
}
content = Base64.decode(content);
if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
let v2rayPlugin = parsed[2];
if (v2rayPlugin) {
proxy.plugin = 'v2ray-plugin';
proxy['plugin-opts'] = JSON.parse(
Base64.decode(v2rayPlugin),
);
}
}
content = `${content}${query}`;
}
userInfoStr = content.split('@')[0];
serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
}
const serverAndPort = serverAndPortArray[1];
const portIdx = serverAndPort.lastIndexOf(':');
proxy.server = serverAndPort.substring(0, portIdx);
proxy.port = serverAndPort.substring(portIdx + 1);
proxy.port = `${serverAndPort.substring(portIdx + 1)}`.match(
/\d+/,
)?.[0];
const userInfo = Base64.decode(content.split('@')[0]).split(':');
const userInfo = userInfoStr.split(':');
proxy.cipher = userInfo[0];
proxy.password = userInfo[1];
@@ -54,7 +89,7 @@ function URI_SS() {
};
break;
case 'v2ray-plugin':
proxy.obfs = 'v2ray-plugin';
proxy.plugin = 'v2ray-plugin';
proxy['plugin-opts'] = {
mode: 'websocket',
host: getIfNotBlank(params['obfs-host']),
@@ -68,6 +103,12 @@ function URI_SS() {
);
}
}
if (/(&|\?)uot=(1|true)/i.test(query)) {
proxy['udp-over-tcp'] = true;
}
if (/(&|\?)tfo=(1|true)/i.test(query)) {
proxy.tfo = true;
}
return proxy;
};
return { name, test, parse };
@@ -150,7 +191,7 @@ function URI_VMess() {
};
const parse = (line) => {
line = line.split('vmess://')[1];
const content = Base64.decode(line);
let content = Base64.decode(line);
if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format
const partitions = content.split(',').map((p) => p.trim());
@@ -202,30 +243,117 @@ function URI_VMess() {
}
return proxy;
} else {
// V2rayN URI format
const params = JSON.parse(content);
let params = {};
try {
// V2rayN URI format
params = JSON.parse(content);
} catch (e) {
// Shadowrocket URI format
// eslint-disable-next-line no-unused-vars
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
content = Base64.decode(base64Line);
for (const addon of qs.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
if (value.indexOf(',') === -1) {
params[key] = value;
} else {
params[key] = value.split(',');
}
}
// eslint-disable-next-line no-unused-vars
let [___, cipher, uuid, server, port] =
/(^[^:]+?):([^:]+?)@(.*):(\d+)$/.exec(content);
params.scy = cipher;
params.id = uuid;
params.port = port;
params.add = server;
}
const server = params.add;
const port = parseInt(getIfPresent(params.port), 10);
const proxy = {
name: params.ps,
name:
params.ps ??
params.remarks ??
params.remark ??
`VMess ${server}:${port}`,
type: 'vmess',
server: params.add,
port: params.port,
cipher: 'auto', // V2rayN has no default cipher! use aes-128-gcm as default.
server,
port,
cipher: getIfPresent(params.scy, 'auto'),
uuid: params.id,
alterId: getIfPresent(params.aid, 0),
tls: params.tls === 'tls' || params.tls === true,
alterId: parseInt(
getIfPresent(params.aid ?? params.alterId, 0),
10,
),
tls: ['tls', true, 1, '1'].includes(params.tls),
'skip-cert-verify': isPresent(params.verify_cert)
? !params.verify_cert
: undefined,
};
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls && proxy.sni) {
proxy.sni = params.sni;
}
// handle obfs
if (params.net === 'ws') {
if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws';
proxy['ws-opts'] = {
path: getIfNotBlank(params.path),
headers: { Host: getIfNotBlank(params.host) },
};
if (proxy.tls && params.host) {
proxy.sni = params.host;
} else if (
['tcp', 'http'].includes(params.net) ||
params.obfs === 'http'
) {
proxy.network = 'http';
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
}
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
try {
const parsedObfs = JSON.parse(transportHost);
const parsedHost = parsedObfs?.Host;
if (parsedHost) {
transportHost = parsedHost;
}
// eslint-disable-next-line no-empty
} catch (e) {}
let transportPath = params.path;
if (proxy.network === 'http') {
if (transportHost) {
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
}
if (transportPath) {
transportPath = Array.isArray(transportPath)
? transportPath[0]
: transportPath;
}
}
if (transportPath || transportHost) {
if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type),
};
} else {
proxy[`${proxy.network}-opts`] = {
path: getIfNotBlank(transportPath),
headers: { Host: getIfNotBlank(transportHost) },
};
}
} else {
delete proxy.network;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
// sni 优先级应高于 host
if (proxy.tls && !proxy.sni && transportHost) {
proxy.sni = transportHost;
}
}
return proxy;
@@ -234,6 +362,319 @@ function URI_VMess() {
return { name, test, parse };
}
function URI_VLESS() {
const name = 'URI VLESS Parser';
const test = (line) => {
return /^vless:\/\//.test(line);
};
const parse = (line) => {
line = line.split('vless://')[1];
let isShadowrocket;
let parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
if (!parsed) {
// eslint-disable-next-line no-unused-vars
let [_, base64, other] = /^(.*?)(\?.*?$)/.exec(line);
line = `${Base64.decode(base64)}${other}`;
parsed = /^(.*?)@(.*?):(\d+)\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
isShadowrocket = true;
}
// eslint-disable-next-line no-unused-vars
let [__, uuid, server, port, ___, addons = '', name] = parsed;
if (isShadowrocket) {
uuid = uuid.replace(/^.*?:/g, '');
}
port = parseInt(`${port}`, 10);
uuid = decodeURIComponent(uuid);
if (name != null) {
name = decodeURIComponent(name);
}
const proxy = {
type: 'vless',
name,
server,
port,
uuid,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.name =
name ??
params.remarks ??
params.remark ??
`VLESS ${server}:${port}`;
proxy.tls = params.security && params.security !== 'none';
if (isShadowrocket && /TRUE|1/i.test(params.tls)) {
proxy.tls = true;
params.security = params.security ?? 'reality';
}
proxy.sni = params.sni || params.peer;
proxy.flow = params.flow;
if (!proxy.flow && isShadowrocket && params.xtls) {
// "none" is undefined
const flow = [undefined, 'xtls-rprx-direct', 'xtls-rprx-vision'][
params.xtls
];
if (flow) {
proxy.flow = flow;
}
}
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
if (['reality'].includes(params.security)) {
const opts = {};
if (params.pbk) {
opts['public-key'] = params.pbk;
}
if (params.sid) {
opts['short-id'] = params.sid;
}
if (Object.keys(opts).length > 0) {
// proxy[`${params.security}-opts`] = opts;
proxy[`${params.security}-opts`] = opts;
}
}
proxy.network = params.type;
if (proxy.network === 'tcp' && params.headerType === 'http') {
proxy.network = 'http';
}
if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs;
}
if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws';
}
if (proxy.network && !['tcp', 'none'].includes(proxy.network)) {
const opts = {};
const host = params.host ?? params.obfsParam;
if (host) {
if (params.obfsParam) {
try {
const parsed = JSON.parse(host);
opts.headers = parsed;
} catch (e) {
opts.headers = { Host: host };
}
} else {
opts.headers = { Host: host };
}
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
} else if (isShadowrocket && params.path) {
if (!['ws', 'http', 'h2'].includes(proxy.network)) {
opts[`${proxy.network}-service-name`] = params.path;
delete params.path;
}
}
if (params.path) {
opts.path = params.path;
}
// https://github.com/XTLS/Xray-core/issues/91
if (['grpc'].includes(proxy.network)) {
opts['_grpc-type'] = params.mode || 'gun';
}
if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts;
}
}
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost) ? httpHost[0] : httpHost;
}
}
return proxy;
};
return { name, test, parse };
}
function URI_Hysteria2() {
const name = 'URI Hysteria2 Parser';
const test = (line) => {
return /^(hysteria2|hy2):\/\//.test(line);
};
const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars
let [__, password, server, ___, port, ____, addons = '', name] =
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `Hysteria2 ${server}:${port}`;
const proxy = {
type: 'hysteria2',
name,
server,
port,
password,
};
const params = {};
for (const addon of addons.split('&')) {
const [key, valueRaw] = addon.split('=');
let value = valueRaw;
value = decodeURIComponent(valueRaw);
params[key] = value;
}
proxy.sni = params.sni;
if (!proxy.sni && params.peer) {
proxy.sni = params.peer;
}
if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs;
}
proxy.ports = params.mport;
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256;
return proxy;
};
return { name, test, parse };
}
function URI_Hysteria() {
const name = 'URI Hysteria Parser';
const test = (line) => {
return /^(hysteria|hy):\/\//.test(line);
};
const parse = (line) => {
line = line.split(/(hysteria|hy):\/\//)[2];
// eslint-disable-next-line no-unused-vars
let [__, server, ___, port, ____, addons = '', name] =
/^(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `Hysteria ${server}:${port}`;
const proxy = {
type: 'hysteria',
name,
server,
port,
};
const params = {};
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/, '-');
value = decodeURIComponent(value);
if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined;
} else if (['insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
} else if (['auth'].includes(key)) {
proxy['auth-str'] = value;
} else if (['mport'].includes(key)) {
proxy['ports'] = value;
} else if (['obfsParam'].includes(key)) {
proxy['obfs'] = value;
} else if (['upmbps'].includes(key)) {
proxy['up'] = value;
} else if (['downmbps'].includes(key)) {
proxy['down'] = value;
} else if (['obfs'].includes(key)) {
// obfs: Obfuscation mode (optional, empty or "xplus")
proxy['_obfs'] = value || '';
} else if (['fast-open', 'peer'].includes(key)) {
params[key] = value;
} else {
proxy[key] = value;
}
}
if (!proxy.sni && params.peer) {
proxy.sni = params.peer;
}
if (!proxy['fast-open'] && params.fastopen) {
proxy['fast-open'] = true;
}
if (!proxy.protocol) {
// protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp")
proxy.protocol = 'udp';
}
return proxy;
};
return { name, test, parse };
}
function URI_TUIC() {
const name = 'URI TUIC Parser';
const test = (line) => {
return /^tuic:\/\//.test(line);
};
const parse = (line) => {
line = line.split(/tuic:\/\//)[1];
// eslint-disable-next-line no-unused-vars
let [__, uuid, password, server, ___, port, ____, addons = '', name] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `TUIC ${server}:${port}`;
const proxy = {
type: 'tuic',
name,
server,
port,
password,
uuid,
};
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/, '-');
value = decodeURIComponent(value);
if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined;
} else if (['allow-insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
} else if (['disable-sni', 'reduce-rtt'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else {
proxy[key] = value;
}
}
return proxy;
};
return { name, test, parse };
}
// Trojan URI format
function URI_Trojan() {
const name = 'URI Trojan Parser';
@@ -242,8 +683,16 @@ function URI_Trojan() {
};
const parse = (line) => {
let [newLine, name] = line.split(/#(.+)/, 2);
const parser = getTrojanURIParser();
const proxy = parser.parse(line);
const proxy = parser.parse(newLine);
if (isNotBlank(name)) {
try {
proxy.name = decodeURIComponent(name);
} catch (e) {
console.log(e);
}
}
return proxy;
};
return { name, test, parse };
@@ -266,10 +715,16 @@ function Clash_All() {
'ss',
'ssr',
'vmess',
'socks',
'socks5',
'http',
'snell',
'trojan',
'tuic',
'vless',
'hysteria',
'hysteria2',
'wireguard',
'ssh',
].includes(proxy.type)
) {
throw new Error(
@@ -278,9 +733,30 @@ function Clash_All() {
}
// handle vmess sni
if (proxy.type === 'vmess') {
if (['vmess', 'vless'].includes(proxy.type)) {
proxy.sni = proxy.servername;
delete proxy.servername;
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost)
? httpHost[0]
: httpHost;
}
}
}
if (proxy.fingerprint) {
proxy['tls-fingerprint'] = proxy.fingerprint;
}
if (proxy['benchmark-url']) {
proxy['test-url'] = proxy['benchmark-url'];
}
if (proxy['benchmark-timeout']) {
proxy['test-timeout'] = proxy['benchmark-timeout'];
}
return proxy;
@@ -324,6 +800,15 @@ function QX_VMess() {
return { name, test, parse };
}
function QX_VLESS() {
const name = 'QX VLESS Parser';
const test = (line) => {
return /^vless\s*=/.test(line.split(',')[0].trim());
};
const parse = (line) => getQXParser().parse(line);
return { name, test, parse };
}
function QX_Trojan() {
const name = 'QX Trojan Parser';
const test = (line) => {
@@ -406,6 +891,15 @@ function Loon_Trojan() {
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_Hysteria2() {
const name = 'Loon Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*Hysteria2/i.test(line.split(',')[0]);
};
const parse = (line) => getLoonParser().parse(line);
return { name, test, parse };
}
function Loon_Http() {
const name = 'Loon HTTP Parser';
@@ -417,6 +911,122 @@ function Loon_Http() {
return { name, test, parse };
}
function Loon_WireGuard() {
const name = 'Loon WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/i.test(line.split(',')[0]);
};
const parse = (line) => {
const name = line.match(
/(^.*?)\s*?=\s*?wireguard\s*?,.+?\s*?=\s*?.+?/i,
)?.[1];
line = line.replace(name, '').replace(/^\s*?=\s*?wireguard\s*/i, '');
let peers = line.match(
/,\s*?peers\s*?=\s*?\[\s*?\{\s*?(.+?)\s*?\}\s*?\]/i,
)?.[1];
let serverPort = peers.match(
/(,|^)\s*?endpoint\s*?=\s*?"?(.+?):(\d+)"?\s*?(,|$)/i,
);
let server = serverPort?.[2];
let port = parseInt(serverPort?.[3], 10);
let mtu = line.match(/(,|^)\s*?mtu\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i)?.[2];
if (mtu) {
mtu = parseInt(mtu, 10);
}
let keepalive = line.match(
/(,|^)\s*?keepalive\s*?=\s*?"?(\d+?)"?\s*?(,|$)/i,
)?.[2];
if (keepalive) {
keepalive = parseInt(keepalive, 10);
}
let reserved = peers.match(
/(,|^)\s*?reserved\s*?=\s*?"?(\[\s*?.+?\s*?\])"?\s*?(,|$)/i,
)?.[2];
if (reserved) {
reserved = JSON.parse(reserved);
}
let dns;
let dnsv4 = line.match(/(,|^)\s*?dns\s*?=\s*?"?(.+?)"?\s*?(,|$)/i)?.[2];
let dnsv6 = line.match(
/(,|^)\s*?dnsv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
if (dnsv4 || dnsv6) {
dns = [];
if (dnsv4) {
dns.push(dnsv4);
}
if (dnsv6) {
dns.push(dnsv6);
}
}
let allowedIps = peers
.match(/(,|^)\s*?allowed-ips\s*?=\s*?"(.+?)"\s*?(,|$)/i)?.[2]
?.split(',')
.map((i) => i.trim());
let preSharedKey = peers.match(
/(,|^)\s*?preshared-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ip = line.match(
/(,|^)\s*?interface-ip\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let ipv6 = line.match(
/(,|^)\s*?interface-ipv6\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
let publicKey = peers.match(
/(,|^)\s*?public-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2];
// https://github.com/MetaCubeX/mihomo/blob/0404e35be8736b695eae018a08debb175c1f96e6/docs/config.yaml#L717
const proxy = {
type: 'wireguard',
name,
server,
port,
ip,
ipv6,
'private-key': line.match(
/(,|^)\s*?private-key\s*?=\s*?"?(.+?)"?\s*?(,|$)/i,
)?.[2],
'public-key': publicKey,
mtu,
keepalive,
reserved,
'allowed-ips': allowedIps,
'preshared-key': preSharedKey,
dns,
udp: true,
peers: [
{
server,
port,
ip,
ipv6,
'public-key': publicKey,
'pre-shared-key': preSharedKey,
'allowed-ips': allowedIps,
reserved,
},
],
};
proxy;
if (Array.isArray(proxy.dns) && proxy.dns.length > 0) {
proxy['remote-dns-resolve'] = true;
}
return proxy;
};
return { name, test, parse };
}
function Surge_SSH() {
const name = 'Surge SSH Parser';
const test = (line) => {
return /^.*=\s*ssh/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_SS() {
const name = 'Surge SS Parser';
const test = (line) => {
@@ -465,36 +1075,151 @@ function Surge_Socks5() {
return { name, test, parse };
}
function Surge_External() {
const name = 'Surge External Parser';
const test = (line) => {
return /^.*=\s*external/.test(line.split(',')[0]);
};
const parse = (line) => {
let parsed = /^\s*(.*?)\s*?=\s*?external\s*?,\s*(.*?)\s*$/.exec(line);
// eslint-disable-next-line no-unused-vars
let [_, name, other] = parsed;
line = other;
// exec = "/usr/bin/ssh" 或 exec = /usr/bin/ssh
let exec = /(,|^)\s*?exec\s*?=\s*"(.*?)"\s*?(,|$)/.exec(line)?.[2];
if (!exec) {
exec = /(,|^)\s*?exec\s*?=\s*(.*?)\s*?(,|$)/.exec(line)?.[2];
}
// local-port = "1080" 或 local-port = 1080
let localPort = /(,|^)\s*?local-port\s*?=\s*"(.*?)"\s*?(,|$)/.exec(
line,
)?.[2];
if (!localPort) {
localPort = /(,|^)\s*?local-port\s*?=\s*(.*?)\s*?(,|$)/.exec(
line,
)?.[2];
}
// args = "-m", args = "rc4-md5"
// args = -m, args = rc4-md5
const argsRegex = /(,|^)\s*?args\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g;
let argsMatch;
const args = [];
while ((argsMatch = argsRegex.exec(line)) !== null) {
if (argsMatch[3] != null) {
args.push(argsMatch[3]);
} else if (argsMatch[4] != null) {
args.push(argsMatch[4]);
}
}
// addresses = "[ipv6]",,addresses = "ipv6", addresses = "ipv4"
// addresses = [ipv6], addresses = ipv6, addresses = ipv4
const addressesRegex =
/(,|^)\s*?addresses\s*?=\s*("(.*?)"|(.*?))(?=\s*?(,|$))/g;
let addressesMatch;
const addresses = [];
while ((addressesMatch = addressesRegex.exec(line)) !== null) {
let ip;
if (addressesMatch[3] != null) {
ip = addressesMatch[3];
} else if (addressesMatch[4] != null) {
ip = addressesMatch[4];
}
if (ip != null) {
ip = `${ip}`.trim().replace(/^\[/, '').replace(/\]$/, '');
}
if (isIP(ip)) {
addresses.push(ip);
}
}
const proxy = {
type: 'external',
name,
exec,
'local-port': localPort,
args,
addresses,
};
return proxy;
};
return { name, test, parse };
}
function Surge_Snell() {
const name = 'Surge Snell Parser';
const test = (line) => {
return /^.*=\s*snell?/.test(line.split(',')[0]);
return /^.*=\s*snell/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_Tuic() {
const name = 'Surge Tuic Parser';
const test = (line) => {
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_WireGuard() {
const name = 'Surge WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_Hysteria2() {
const name = 'Surge Hysteria2 Parser';
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function isIP(ip) {
return isIPv4(ip) || isIPv6(ip);
}
export default [
URI_SS(),
URI_SSR(),
URI_VMess(),
URI_VLESS(),
URI_TUIC(),
URI_Hysteria(),
URI_Hysteria2(),
URI_Trojan(),
Clash_All(),
Surge_SSH(),
Surge_SS(),
Surge_VMess(),
Surge_Trojan(),
Surge_Http(),
Surge_Snell(),
Surge_Tuic(),
Surge_WireGuard(),
Surge_Hysteria2(),
Surge_Socks5(),
Surge_External(),
Loon_SS(),
Loon_SSR(),
Loon_VMess(),
Loon_Vless(),
Loon_Hysteria2(),
Loon_Trojan(),
Loon_Http(),
Loon_WireGuard(),
QX_SS(),
QX_SSR(),
QX_VMess(),
QX_VLESS(),
QX_Trojan(),
QX_Http(),
QX_Socks5(),

View File

@@ -35,7 +35,7 @@ const grammars = String.raw`
}
}
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http) {
start = (shadowsocksr/shadowsocks/vmess/vless/trojan/https/http/hysteria2) {
return proxy;
}
@@ -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;
@@ -114,7 +117,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 +145,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; }
@@ -167,6 +173,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
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/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;
@@ -112,7 +115,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 +143,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; }
@@ -165,6 +171,9 @@ tls_verification = comma "skip-cert-verify" equals flag:bool { proxy["skip-cert-
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
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,18 +38,18 @@ 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)* {
(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";
// handle ssr obfs
@@ -80,7 +80,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 +91,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 +149,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,6 +162,14 @@ 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; }
@@ -166,6 +181,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,18 +36,18 @@
}
}
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)* {
(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";
// handle ssr obfs
@@ -78,7 +78,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 +89,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 +147,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,6 +160,14 @@ 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; }
@@ -164,6 +179,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 {
@@ -147,6 +184,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
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 +192,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 +213,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 {
@@ -145,6 +182,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
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 +190,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 +211,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,23 @@ params = "?" head:param tail:("&"@param)* {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
proxy.network = params["type"]
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
@@ -94,7 +111,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,23 @@ params = "?" head:param tail:("&"@param)* {
proxy.network = "ws";
$set(proxy, "ws-opts.path", params["wspath"]);
}
if (params["type"]) {
proxy.network = params["type"]
if (['grpc'].includes(proxy.network)) {
proxy[proxy.network + '-opts'] = {
'grpc-service-name': params["serviceName"],
'_grpc-type': params["mode"],
};
} else {
if (params["path"]) {
$set(proxy, proxy.network+"-opts.path", decodeURIComponent(params["path"]));
}
if (params["host"]) {
$set(proxy, proxy.network+"-opts.headers.Host", decodeURIComponent(params["host"]));
}
}
}
proxy.udp = toBool(params["udp"]);
proxy.tfo = toBool(params["tfo"]);
@@ -92,7 +109,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,22 @@ 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
];
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 +40,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 };
}
@@ -103,4 +121,4 @@ function FullConfig() {
return { name, test, parse };
}
export default [HTML(), Base64Encoded(), Clash(), SSD(), FullConfig()];
export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];

View File

@@ -7,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:
@@ -77,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'],
@@ -108,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) => {
@@ -122,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;
});
@@ -294,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) => {
@@ -305,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',
},
@@ -337,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,
@@ -354,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',
},
@@ -378,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?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?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) {
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',
@@ -392,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);
@@ -400,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(
@@ -416,11 +542,41 @@ 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.server = server;
p.port = port;
p.resolved = true;
}
} else {
p.server = results[p.server];
p.resolved = true;
}
} 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;
}
});
},
};
}
@@ -522,7 +678,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) => {
@@ -533,7 +689,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;
},
@@ -565,8 +743,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]);
}
@@ -577,8 +779,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;
}
@@ -625,6 +858,14 @@ function removeFlag(str) {
}
function createDynamicFunction(name, script, $arguments) {
const flowUtils = {
getFlowField,
getFlowHeaders,
parseFlowHeaders,
flowTransfer,
validCheck,
getRmainingDays,
};
if ($.env.isLoon) {
return new Function(
'$arguments',
@@ -635,6 +876,8 @@ function createDynamicFunction(name, script, $arguments) {
'$notification',
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)(
$arguments,
@@ -648,6 +891,8 @@ function createDynamicFunction(name, script, $arguments) {
$notification,
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
} else {
return new Function(
@@ -656,7 +901,18 @@ function createDynamicFunction(name, script, $arguments) {
'lodash',
'ProxyUtils',
'scriptResourceCache',
'flowUtils',
'produceArtifact',
`${script}\n return ${name}`,
)($arguments, $, lodash, ProxyUtils, scriptResourceCache);
)(
$arguments,
$,
lodash,
ProxyUtils,
scriptResourceCache,
flowUtils,
produceArtifact,
);
}
}

View File

@@ -2,36 +2,168 @@ 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 (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -0,0 +1,185 @@
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 (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => ' - ' + JSON.stringify(proxy) + '\n')
.join('');
};
return { type, produce };
}

View File

@@ -1,10 +1,15 @@
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';
@@ -14,11 +19,18 @@ function JSON_Producer() {
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,10 @@ export default function Loon_Producer() {
return vless(proxy);
case 'http':
return http(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 +32,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 +83,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 +111,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 +123,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 +156,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 +166,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 +225,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 +289,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 +314,105 @@ function http(proxy) {
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
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,7 @@ import { isPresent, Result } from './utils';
const targetPlatform = 'QX';
export default function QX_Producer() {
const produce = (proxy) => {
const produce = (proxy, type, opts = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
@@ -17,6 +17,14 @@ export default function QX_Producer() {
return http(proxy);
case 'socks5':
return socks5(proxy);
case 'vless':
if (opts['include-unsupported-proxy']) {
return vless(proxy);
} else {
throw new Error(
`Platform ${targetPlatform}(App Store Release) does not support proxy type: ${proxy.type}`,
);
}
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -29,7 +37,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 +99,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 +133,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 +171,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 +197,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 +214,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 +248,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 +291,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 +310,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 +351,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 +477,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 +511,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 +538,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 +572,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 +585,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,189 @@
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 (isPresent(proxy, 'tls') && typeof proxy.tls !== 'boolean') {
delete proxy.tls;
}
delete proxy.subName;
delete proxy.collectionName;
if (
['grpc'].includes(proxy.network) &&
proxy[`${proxy.network}-opts`]
) {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
}
return proxy;
});
return type === 'internal'
? list
: 'proxies:\n' +
list
.map((proxy) => {
return ' - ' + JSON.stringify(proxy) + '\n';
})
.join('');
};
return { type, produce };
}

View File

@@ -0,0 +1,795 @@
import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
return parsedProxy;
};
const wireguardParser = (proxy = {}) => {
const local_address = ['ip', 'ipv6']
.map((i) => proxy[i])
.filter((i) => i)
.map((i) => (/\\/.test(i) ? i : `${i}/32`));
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.push(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);
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(list, null, 2);
};
return { type, produce };
}

View File

@@ -2,30 +2,275 @@ 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.fingerprint = proxy['tls-fingerprint'];
}
delete proxy['tls-fingerprint'];
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;
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,19 @@
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 = {}) => {
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy);
@@ -19,6 +27,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 +50,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 +108,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`);
}
}
@@ -58,6 +121,62 @@ function shadowsocks(proxy) {
// 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();
}
@@ -67,6 +186,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);
@@ -94,6 +221,44 @@ function trojan(proxy) {
// 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();
}
@@ -103,6 +268,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);
@@ -137,10 +310,113 @@ function vmess(proxy) {
// 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';
@@ -148,6 +424,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']}`,
@@ -169,6 +453,44 @@ function http(proxy) {
// 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();
}
@@ -180,6 +502,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']}`,
@@ -203,6 +533,44 @@ function socks5(proxy) {
// 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();
}
@@ -213,25 +581,433 @@ 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();
}
@@ -248,7 +1024,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,10 +1,19 @@
/* 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;
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}`;
@@ -35,6 +44,12 @@ export default function URI_Producer() {
);
}
}
if (proxy['udp-over-tcp']) {
result = `${result}${proxy.plugin ? '&' : '?'}uot=1`;
}
if (proxy.tfo) {
result = `${result}${proxy.plugin ? '&' : '?'}tfo=1`;
}
result += `#${encodeURIComponent(proxy.name)}`;
break;
case 'ssr':
@@ -55,28 +70,342 @@ 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';
}
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 vlessTransport = `&type=${encodeURIComponent(
proxy.network,
)}`;
if (['grpc'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
}
let vlessTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let vlessTransportPath = proxy[`${proxy.network}-opts`]?.path;
let vlessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vlessTransportPath) {
vlessTransport += `&path=${encodeURIComponent(
Array.isArray(vlessTransportPath)
? vlessTransportPath[0]
: vlessTransportPath,
)}`;
}
if (vlessTransportHost) {
vlessTransport += `&host=${encodeURIComponent(
Array.isArray(vlessTransportHost)
? vlessTransportHost[0]
: vlessTransportHost,
)}`;
}
if (vlessTransportServiceName) {
vlessTransport += `&serviceName=${encodeURIComponent(
vlessTransportServiceName,
)}`;
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?security=${encodeURIComponent(
security,
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'trojan':
let trojanTransport = '';
if (proxy.network) {
trojanTransport = `&type=${proxy.network}`;
if (['grpc'].includes(proxy.network)) {
let trojanTransportServiceName =
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
)}`;
}
let trojanTransportPath =
proxy[`${proxy.network}-opts`]?.path;
let trojanTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (trojanTransportPath) {
trojanTransport += `&path=${encodeURIComponent(
Array.isArray(trojanTransportPath)
? trojanTransportPath[0]
: trojanTransportPath,
)}`;
}
if (trojanTransportHost) {
trojanTransport += `&host=${encodeURIComponent(
Array.isArray(trojanTransportHost)
? trojanTransportHost[0]
: trojanTransportHost,
)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${
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;
}
}
return result;
};

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,8 +1,14 @@
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);
@@ -30,23 +36,83 @@ 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);
@@ -54,17 +120,34 @@ async function doSync() {
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[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);
$.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,13 +244,14 @@ 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);
}

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

@@ -20,6 +20,43 @@ async function downloadSubscription(req, res) {
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
let {
url,
ua,
content,
mergeSources,
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
} = 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);
@@ -29,15 +66,71 @@ async function downloadSubscription(req, res) {
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') {
res.set('Content-Type', 'application/json;charset=utf-8').send(
@@ -50,15 +143,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}`,
),
);
}
@@ -87,12 +180,34 @@ async function downloadCollection(req, res) {
$.info(`正在下载组合订阅:${name}`);
let { ignoreFailedRemoteSub, produceType, includeUnsupportedProxy } =
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({
type: 'collection',
name,
platform,
ignoreFailedRemoteSub,
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
},
});
// forward flow header from the first subscription in this collection
@@ -100,12 +215,59 @@ 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') {
@@ -126,7 +288,7 @@ async function downloadCollection(req, res) {
new InternalServerError(
'INTERNAL_SERVER_ERROR',
`Failed to download collection: ${name}`,
`Reason: ${JSON.stringify(err)}`,
`Reason: ${err.message ?? err}`,
),
);
}

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,297 @@ 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(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误:${err}!进度--${
100 * (processed / subnames.length).toFixed(1)
}%`,
);
}
// 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,45 +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 uploadMatch = flowHeaders.match(/upload=(-?)(\d+)/)
const upload = Number(uploadMatch[1] + uploadMatch[2]);
const downloadMatch = flowHeaders.match(/download=(-?)(\d+)/)
const download = Number(downloadMatch[1] + downloadMatch[2]);
const total = Number(flowHeaders.match(/total=(\d+)/)[1]);
// optional expire timestamp
const match = flowHeaders.match(/expire=(\d+)/);
const expires = match ? Number(match[1]) : undefined;
success(res, { expires, total, usage: { upload, download } });
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,
@@ -93,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(
@@ -102,6 +172,7 @@ function createSubscription(req, res) {
`Subscription ${sub.name} already exists.`,
),
);
return;
}
allSubs.push(sub);
$.write(allSubs, SUBS_KEY);
@@ -202,3 +273,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,32 +357,197 @@ 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);
@@ -204,61 +555,107 @@ async function syncAllArtifacts(_, res) {
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[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);
$.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,
@@ -266,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(
@@ -283,4 +696,4 @@ async function syncArtifact(req, res) {
}
}
export { produceArtifact };
export { produceArtifact, syncArtifacts };

View File

@@ -1,44 +1,143 @@
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) {
const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = hex_md5(ua + url);
export default async function download(rawUrl, ua, timeout, proxy) {
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 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) {
$.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);
}
result = body;
} catch (e) {
throw new Error(`无法下载 URL ${url}: ${e.message ?? e}`);
}
});
}
// 检查订阅有效性
if ($arguments?.validCheck) {
await validCheck(
parseFlowHeaders(
await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy,
),
),
);
}
if (!isNode) {
tasks.set(id, result);

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

@@ -0,0 +1,61 @@
import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api';
const {
isNode,
isQX,
isLoon,
isSurge,
isStash,
isShadowRocket,
isLanceX,
isEgern,
} = 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';
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 (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'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024
? { 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

@@ -155,7 +155,7 @@ export function getFlag(name) {
'🇲🇴': ['Macao', '澳门', '澳門', 'CTM'],
'🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来西亚', '馬來西亞', '吉隆坡', '大馬'],
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'],
@@ -213,11 +213,16 @@ export function getFlag(name) {
'🇹🇼': [
'Taiwan',
'台湾',
'臺灣',
'台灣',
'中華民國',
'中华民国',
'台北',
'台中',
'新北',
'彰化',
'台',
'臺',
'Taipei',
],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
@@ -371,7 +376,7 @@ export function getFlag(name) {
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET'],
'🇹🇼': ['TW', 'TWN', 'CHT', 'HINET', 'ROC'],
'🇺🇦': ['UA', 'UKR'],
'🇺🇸': ['US', 'USA', 'LAX', 'SFO'],
'🇺🇾': ['UY', 'URY'],

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,22 +1,39 @@
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
let ua = '';
for (let k of keys) {
if (/USER-AGENT/i.test(k)) {
UA = headers[k];
ua = UA.toLowerCase();
break;
}
}
if (UA.indexOf('Quantumult%20X') !== -1) {
return 'QX';
} else if (UA.indexOf('Surfboard') !== -1) {
return 'Surfboard';
} else if (UA.indexOf('Surge Mac') !== -1) {
return 'SurgeMac';
} else if (UA.indexOf('Surge') !== -1) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Shadowrocket') !== -1) {
return 'ShadowRocket';
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';
}

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

View File

@@ -10,7 +10,17 @@ class ResourceCache {
if (!$.read(SCRIPT_RESOURCE_CACHE_KEY)) {
$.write('{}', SCRIPT_RESOURCE_CACHE_KEY);
}
this.resourceCache = JSON.parse($.read(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();
}

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;

View File

@@ -6,6 +6,8 @@ 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;
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
@@ -49,27 +51,32 @@ export class OpenAPI {
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}`));
}
}
}
@@ -80,14 +87,17 @@ export class OpenAPI {
if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name);
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),
@@ -183,6 +193,32 @@ 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}`);
});
}
}
}
@@ -217,7 +253,16 @@ export class OpenAPI {
}
export function ENV() {
return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket };
return {
isQX,
isLoon,
isSurge,
isNode,
isStash,
isShadowRocket,
isEgern,
isLanceX,
};
}
export function HTTP(defaultOptions = { baseURL: '' }) {
@@ -271,31 +316,53 @@ 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,
});
});
});
}
let timeoutid;
const timer = timeout
? new Promise((_, reject) => {
// console.log(`[request timeout] ${timeout}ms`);
timeoutid = setTimeout(() => {
events.onTimeout();
return reject(
@@ -308,7 +375,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,10 +1,11 @@
#!name=Sub-Store
#!desc=高级订阅管理工具
#!desc=高级订阅管理工具. 定时任务默认为每天 23 点 55 分
#!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
@@ -16,4 +17,4 @@ hostname=sub.store
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
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 分",
"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. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
> 最新 Surge iOS TestFlight 版本应该没有内存问题了 可以大胆尝试带 ability 参数版本
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.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.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 分
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,15 @@
#!name=Sub-Store(β)
#!desc=支持最新 Surge iOS TestFlight 版本的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *
#!category=订阅管理
#!arguments=ability:http-client-policy,cronexp:55 23 * * *,sync:"Sub-Store Sync"
#!arguments-desc="\n1⃣ ability\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n⚠ Surge 上时候可能会爆内存\n不需要使用的时候应该关闭\n填写任意其他值关闭\n\n2⃣ cronexp\n同步配置定时任务\n默认为每天 23 点 55 分\n\n3⃣ sync\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务"
[MITM]
hostname = %APPEND% sub.store
[Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability="{{{ability}}}"
Sub-Store 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
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -0,0 +1,13 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!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 参数版本, 可能会爆内存, 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用不带 ability 参数版本. 定时任务默认为每天 23 点 55 分
#!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,12 @@
#!name=Sub-Store
#!desc=高级订阅管理工具 @Peng-YM
#!desc=高级订阅管理工具 @Peng-YM 无 ability 参数版本,不会爆内存, 如果需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 可以用带 ability 参数. 定时任务默认为每天 23 点 55 分
#!category=订阅管理
[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=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=0 0 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
Sub-Store Sync=type=cron,cronexp=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

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="
}
}
}

205
scripts/demo.js Normal file
View File

@@ -0,0 +1,205 @@
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. 节点字段 `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 解析和生成
// }
// 示例: 从 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 )
// })
// // 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
}

1
web

Submodule web deleted from b10b708c34