Compare commits

..

827 Commits

Author SHA1 Message Date
xream
0d41eb467f feat: 某些域名仅支持从国内 DNS 解析正确结果, 为方便部署在海外的用户, 使用国内 DNS 解析时, ECS IP 指定为国内 IP 2024-05-18 22:24:58 +08:00
xream
ba1c91a7a5 chore: 文案 2024-05-17 17:49:10 +08:00
xream
30fa87c172 doc: Shadowrocket 模块 2024-05-15 20:58:55 +08:00
xream
1eaa33948b chore: bump release version 2024-05-14 20:45:39 +08:00
xream
619e256ed8 Merge pull request #322 from onejibang/master
chore: Change network request method
2024-05-14 20:45:00 +08:00
onejibang
b46209e164 chore: Change network request method 2024-05-14 16:51:05 +08:00
xream
a1ba4e273e chore: bump release version 2024-05-13 19:44:27 +08:00
xream
bfc95ed92a Merge pull request #320 from onejibang/feature-gui-for-cores
feat: 适配GUI.for.Cores项目组下的客户端程序
2024-05-13 19:41:45 +08:00
xream
32f591ec56 Merge branch 'master' into feature-gui-for-cores 2024-05-13 19:41:28 +08:00
onejibang
cea16d8c44 chore: Canonical variable name 2024-05-13 19:22:33 +08:00
onejibang
93a1ba7b50 feat: support HEAD method 2024-05-13 19:12:22 +08:00
xream
e6d1aa1150 feat: 使用了自定义缓存 cacheKey 的远程订阅 调整为乐观缓存 2024-05-13 17:08:17 +08:00
onejibang
26e83798da feat: Provide virtual disk operation API 2024-05-13 16:35:05 +08:00
xream
b083d2d840 feat: Node.js 版支持 MMDB, 通过环境变量或在脚本中传入数据库文件路径, 可使用 ipaso 和 geoip 方法 2024-05-12 23:17:11 +08:00
onejibang
cf35afcab2 Adapted for GUI.for.Cores 2024-05-11 17:32:01 +08:00
xream
d073dfeef8 feat: 支持 Trojan, VMess, VLESS httpupgrade(暂不支持 Shadowsocks v2ray-plugin) 2024-05-10 10:15:11 +08:00
Peng-YM
f970ea3361 Update README.md 2024-05-09 09:38:36 +08:00
xream
630bac0575 fix: 简单修复 SS URI 多参数拼接 2024-05-05 02:14:55 +08:00
xream
7f3cb2b191 feat: 当无插件参数时, 去除 SS URI 输出中的 / 以兼容部分客户端 2024-05-05 02:11:50 +08:00
xream
92e1e4a0fb feat: ProxyUtils 中增加 Gist 类; 补充 demo.js 中的示例 2024-05-04 21:35:27 +08:00
xream
3b85d313fe fix: 兼容不规范的 QX URI 2024-05-03 03:56:59 +08:00
xream
c91d8e28e4 fix: 哪吒探针在线时长 2024-04-30 15:39:14 +08:00
xream
8cbb4492be feat: 全部是 WireGuard 节点的订阅, 支持输出为 Surge 模块 2024-04-25 16:55:32 +08:00
xream
6f7da57e3a fix: 旗帜操作中将 🏴‍☠️ 🏳️‍🌈 也视为已有旗帜并在删除后添加新旗帜 2024-04-25 13:38:01 +08:00
xream
2586c29746 fix: 处理手动删除 Gist 之后, Sub-Store 侧重新同步的逻辑 2024-04-23 09:28:39 +08:00
xream
6f7fe8204b feat: 支持完整导出和导入 Sub-Store 单条订阅数据 2024-04-22 15:15:57 +08:00
xream
bafaf07743 Merge pull request #314 from eric-gitta-moore/master
feat: script ip-flag for node
2024-04-22 13:09:33 +08:00
Eric Moore
9962eb0947 feat: script ip-flag for node 2024-04-22 12:59:06 +08:00
xream
ac5232a7bc ci: restore the functionality of generating conventional changelog 2024-04-22 04:25:19 +08:00
xream
2301ccbfb5 fix: 修复对不规范的节点名称的处理 2024-04-22 02:51:44 +08:00
xream
0b5761e5fc feat: QX 输出正式支持 VLESS 2024-04-22 02:15:54 +08:00
xream
3ab21b0e26 feat: 支持 Loon SOCKS5/SOCKS5-TLS 2024-04-21 12:36:11 +08:00
xream
89ab72e46c doc: README 2024-04-21 11:32:23 +08:00
xream
18bd6526d0 chore: 增加探针版本(没有自定义的必要吧 默认为 0.0.1) 2024-04-20 07:45:49 +08:00
xream
c7329c32eb feat: 哪吒探针网络监控接口支持用参数传入检测次数; 节点字段上自定义的多个次数, 只取最大值 2024-04-19 06:27:45 +08:00
xream
4819ae95e4 feat: 哪吒探针网络监控接口提示不兼容的节点, 支持传入节点名 2024-04-19 05:57:26 +08:00
xream
370d228b04 feat: 订阅兼容哪吒探针网络监控接口(Loon/Surge 可输出节点延迟) 2024-04-19 05:16:24 +08:00
xream
d092916168 fix: sing-box wireguard 2024-04-17 11:36:23 +08:00
xream
0c93de48ab feat: GEO 增加 TYO 2024-04-17 05:50:50 +08:00
xream
274aa50373 ci: GitHub Actions: Transitioning from Node 16 to Node 20 2024-04-17 01:12:07 +08:00
xream
e24de8d0b6 feat: 支持 WireGuard URI 输入和输出 2024-04-17 00:56:53 +08:00
xream
93a5ce6c3b feat: 支持 dialer-proxy, detour 2024-04-14 21:34:45 +08:00
xream
cb66c8daa2 feat: fancy-characters 增加 modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代) 2024-04-12 22:39:59 +08:00
xream
f4cdc953e6 feat: 支持设置并在远程订阅失败时读取最近一次成功的缓存 2024-04-09 20:49:42 +08:00
xream
2a1c2eb9df chore: 处理订阅输出哪吒探针兼容响应的 Uptime 字段 2024-04-09 13:48:33 +08:00
xream
6217c2e5cd fix: 修复 sing-box wireguard reserved 2024-04-07 19:48:08 +08:00
xream
f90d9c2fd1 chore: Surge 模块文案 2024-04-07 16:40:09 +08:00
xream
3e952e9e88 chore: Surge 默认模块更新为支持编辑参数的版本 2024-04-06 19:40:30 +08:00
xream
a81b55f752 doc: demo.js 2024-04-05 13:46:07 +08:00
xream
33652af516 feat: 订阅支持输出哪吒探针兼容响应; 清理输出数据; 增加内部数据字段 2024-04-05 13:37:15 +08:00
xream
2bca669930 fix: 修复 SS URI 解析错误 2024-04-04 16:52:46 +08:00
xream
f1bf0e1e8d fix: 修复 Tencent DNS 解析 2024-04-04 15:26:19 +08:00
xream
16b9cd9aaf feat: GEO 增加 AMS 2024-04-03 00:45:48 +08:00
xream
32eb069ab2 feat: GEO 增加 JNB, SJC, SEL 2024-04-02 21:06:56 +08:00
xream
4c9f8011c7 fix: 修复内蒙古识别为蒙古的问题 2024-04-02 20:56:07 +08:00
xream
bd26b0a561 feat: 区域过滤增加韩国德国 2024-04-02 20:40:13 +08:00
xream
958d1e52c8 chore: 日志输出增加订阅的来源 User-Agent 2024-03-31 10:59:49 +08:00
xream
e7a2e60963 feat: sing-box 订阅格式修改(如需原始格式 请使用 target=sing-box&produceType=internal); 清理 Clash 系无效字段 2024-03-31 09:36:32 +08:00
xream
fa6a274f79 feat: ProxyUtils 增加 getFlag, getISO 方法 2024-03-31 08:20:51 +08:00
xream
e40b3f88d5 chore: geo 增加关键词 德意志 2024-03-31 06:59:31 +08:00
xream
163ad9ee09 feat: JSON 输出支持 produceType internal 2024-03-31 04:45:57 +08:00
xream
abb6f2dec1 feat: 处理 sni off 的情况. 若出现问题, 麻烦大家及时反馈 2024-03-30 01:12:34 +08:00
xream
56870bbd5f fix: 修复组合订阅子订阅失败导致预览失败 2024-03-26 14:59:24 +08:00
xream
efbc6ecd84 feat: 流量单位显示由 EB 提升到 YB 2024-03-25 03:50:47 +08:00
xream
c27c589024 chore: 调整部分日志 2024-03-25 02:47:06 +08:00
xream
0efed4f1a0 feat: 处理传入 httpClient 的 timeout 参数 2024-03-24 07:28:16 +08:00
xream
e3a514d1fb feat: hysteria2 支持 mport, clash.meta(mihomo) 支持 ports 2024-03-21 20:07:24 +08:00
xream
64478c7a27 feat: 刷新时 清除所有缓存 2024-03-21 02:37:21 +08:00
xream
dc8f19f350 doc: demo.js 2024-03-21 01:52:42 +08:00
xream
b4ccfc7e07 chore: Sub-Store Simple 脚本增加脚本超时(默认 120) 可能会影响某些逻辑 待观察 2024-03-19 23:48:26 +08:00
xream
3f1940630a chore: 增加 gist 错误日志 2024-03-19 21:30:52 +08:00
xream
5a0bdb1276 doc: demo.js 2024-03-18 20:24:18 +08:00
xream
a1b86e26a2 chore: 增加上传同步配置的详细日志 2024-03-16 02:47:01 +08:00
xream
6ec8c29f6a feat: 规则中处理 GEOIP/GEOSITE, Loon 已支持 SRC-PORT/DEST-PORT/PROTOCOL 2024-03-15 08:49:36 +08:00
xream
bbb9602f9f release: backend version 2.14.256 2024-03-15 08:12:06 +08:00
xream
6db6153672 Merge pull request #295 from makaspacex/master
[Feat]规则转换增加对GEOIP与GEOSITE的支持
2024-03-15 08:10:30 +08:00
makabaka
b66189948a 规则转换增加对GEOIP与GEOSITE的支持 2024-03-14 22:07:45 +08:00
xream
2611dccc73 feat: 支持设置查询远程订阅流量信息时的 User-Agent 2024-03-14 19:45:39 +08:00
xream
25d3cf6ca4 feat: 通过代理/节点/策略获取订阅 现已支持 Surge, Loon, Stash, Shadowrocket, QX, Node.js 2024-03-14 01:54:07 +08:00
xream
3637c5eb74 feat: SSH 协议跟进 clash.meta(mihomo) 的修改 2024-03-13 16:24:30 +08:00
xream
80d46597b4 feat: 支持使用代理/节点/策略获取订阅 2024-03-13 05:33:52 +08:00
xream
ca65e4209e feat: 支持自定义订阅流量信息 2024-03-12 01:17:56 +08:00
xream
53bb4866e7 fix: 修复订阅流量传递 2024-03-12 00:55:30 +08:00
xream
09495fa607 fix: 修复重置天数微妙的偏差 2024-03-11 19:33:51 +08:00
xream
4b27d40602 feat: 订阅支持开始日期和重置周期 2024-03-11 13:39:52 +08:00
xream
518de2e919 feat: 订阅支持每月重置天数 2024-03-10 23:08:56 +08:00
xream
078bf228de feat: produceArtifact 方法支持传入自定义 subscription; VLESS 非 reality 删除空 flow 2024-03-10 17:22:25 +08:00
xream
aaef97cf5d feat: SSH 新增 clash.meta(mihomo), 调整 Surge 和 sing-box 2024-03-08 19:01:01 +08:00
xream
7beff4013f feat: 订阅列表的流量信息兼容远程和本地合并的情况, 排除设置了不查询订阅信息的链接 2024-03-08 18:40:44 +08:00
xream
23cf81d0a5 feat: Node.js 版 /api/utils/env 增加 meta 信息 2024-03-08 14:20:55 +08:00
xream
572f2f5533 feat: OpenAPI 增加 isEgern, isLanceX; /api/utils/env 增加 meta 信息 2024-03-08 13:56:59 +08:00
xream
1c6d761e09 fix: 修复 Surge WireGuard allowed-ips 双引号 2024-03-07 17:24:49 +08:00
xream
437297b8b0 feat: 增加下载缓存阈值 2024-03-05 05:03:17 +08:00
xream
ca437865e6 feat: 域名解析新增 IP4P, 支持禁用缓存 2024-03-05 01:01:46 +08:00
xream
739100c873 feat: Stash/clash.meta(mihomo) 支持 interface-name 字段 2024-03-04 11:43:07 +08:00
xream
a4384f4f13 fix: 修复 Clash 节点名为 binary 的情况 2024-03-03 14:33:49 +08:00
xream
468d136f0e ci: git push assets to "release" branch 2024-02-28 23:07:16 +08:00
xream
b0c1157fe1 feat: 调整规则参数 2024-02-28 22:36:22 +08:00
xream
56626dabc7 Merge pull request #289 from makaspacex/master
修复IP-CIDR的option错误
2024-02-28 22:19:11 +08:00
makabaka
2a87f7b3c3 修复IP-CIDR的option错误 2024-02-28 20:06:03 +08:00
xream
81adfbc461 doc: README 2024-02-23 22:03:10 +08:00
xream
e04217c50d fix: 参数对象 2024-02-23 20:15:37 +08:00
xream
391a5aa2e4 chore: 调整默认定时为每天 23 点 55 分 2024-02-23 20:05:48 +08:00
xream
2f3b42f552 fix: Surge TUIC server-cert-fingerprint-sha256 2024-02-22 11:37:18 +08:00
xream
76302f9d53 fix: sing-box wireguard reserved 2024-02-21 19:09:23 +08:00
xream
1924e9735c feat: Surge 参数 tos allow-other-interface interface test-udp test-timeout hybrid; Stash 参数 benchmark-timeout; Surge 新增 SSH(仅密码方式), sing-box 新增 SSH 2024-02-18 05:25:31 +08:00
xream
6a8cee3cd5 feat: 节点名称为空时, 添加默认节点名称 2024-02-17 17:41:54 +08:00
xream
9b2209cc8b chore: sing-box grpc servicename 应为字符串 2024-02-17 12:03:25 +08:00
xream
c585785c50 fix: 兼容不规范的 VLESS URI 2024-02-17 11:39:04 +08:00
xream
c3e5da7ee4 fix: 兼容不规范的 VLESS URI 2024-02-17 10:50:33 +08:00
xream
2ecad8bbb8 feat: 旗帜操作(支持更多选项) 2024-02-17 08:32:37 +08:00
xream
a642213928 feat: 脚本操作完整支持 /api/file/name 的内部文件调用路径 2024-02-15 03:30:53 +08:00
xream
f85e360ea8 feat: 同步配置前, 预处理订阅, 防止同时请求过多 2024-02-14 19:51:44 +08:00
xream
1b948cdf52 doc: 补充文档 2024-02-13 15:30:13 +08:00
xream
556d5f393c feat: 带参数 includeUnsupportedProxy 时, 支持输出 Surge WireGuard Section 2024-02-13 15:21:29 +08:00
xream
a8c05207c0 chore: dev 调整 2024-02-11 01:10:01 +08:00
xream
e97fb1e6d9 fix: 兼容更多 Trojan URI 格式 2024-02-08 23:06:39 +08:00
xream
e40b9a77c4 fix: 修复 Loon UDP 参数 2024-02-07 15:14:24 +08:00
xream
df0ac8a218 fix: 缓存不合法时即刻重置 2024-02-07 01:14:52 +08:00
xream
36377f3c20 fix: 修复 Surge 协议 test-url 字段 2024-02-06 22:59:54 +08:00
xream
47f26bdac8 feat: Loon 解析器支持参数 ua=clash.meta&timeout=3000, 支持从链接重新获取 2024-02-06 21:29:10 +08:00
xream
a8a89ee2a2 fix: 修复 PassWall VLESS URI 兼容性 2024-02-06 00:30:53 +08:00
xream
199c5fb337 fix: 修复域名解析 2024-02-05 13:19:22 +08:00
xream
df505616ec chore: Stash 覆写增加图标 2024-02-05 11:40:02 +08:00
xream
c5b11f8b36 fix: 修复过滤非法节点功能 2024-02-04 01:22:27 +08:00
xream
b19b49d2fa chore: 端口号允许为 0 2024-02-03 23:59:26 +08:00
xream
395c6e4e4a chore: 在 bundle 文件顶部添加版本号 2024-02-03 22:34:58 +08:00
xream
ae1c738f70 chore: 开发流程使用 esbuild 2024-02-03 21:30:27 +08:00
xream
02d54208b0 feat: 支持 Hysteria(v1) URI 2024-02-03 17:54:37 +08:00
xream
5d3fc499ce feat: 支持 TUIC v5 URI 2024-02-03 16:17:26 +08:00
xream
d23bc7663e feat: 支持 Loon fast-open 2024-02-02 22:55:00 +08:00
xream
15704ea1c9 chore: 增加 esbuild bundle(暂不启用 仅本地使用) 2024-02-02 19:53:14 +08:00
xream
68cb393a7e feat: 更新 Surge, Loon, QX 输入的 Shadowsocks cipher 2024-02-02 13:23:59 +08:00
xream
2e99f28aa5 feat: 输出时校验 Surge, Surfboard, Loon, QX Shadowsocks cipher 2024-02-02 13:04:32 +08:00
xream
1a18e65e47 chore: VLESS HTTP 传输层增加默认 path 2024-02-02 12:44:08 +08:00
xream
bbd5341d7a chore: 脚本说明 2024-02-02 11:58:01 +08:00
xream
623802d73f chore: 增加脚本说明 2024-02-01 21:59:24 +08:00
xream
19920dbfa3 chore: 增加脚本说明 2024-02-01 21:58:30 +08:00
xream
8764e01d7e chore: 增加脚本说明 2024-02-01 21:58:17 +08:00
xream
31b6dd0507 chore: YAML 解析兼容(保持类型) 2024-01-31 03:27:07 +08:00
xream
a84007d39e chore: Clash 系输出中 tls 字段存在且不为布尔值时, 删除该字段防止客户端解析报错 2024-01-30 23:32:39 +08:00
xream
751e50bf99 chore: YAML 解析兼容 2024-01-30 22:23:57 +08:00
xream
a91f978042 feat: 远程订阅 URL 新增参数 validCheck 将检查订阅有效期和剩余流量 2024-01-30 14:14:57 +08:00
xream
1248e6b32a fix: v2ray-plugin 2024-01-30 03:24:42 +08:00
xream
d0f255d9c6 feat: h2 传输层修正 host 为数组, path 不为数组 2024-01-30 02:17:58 +08:00
xream
060415584e feat: 支持 tls 的 plugin 中跳过证书验证; Surge Shadow TLS 默认版本为 2; sing-box 支持 Shadowsocks Shadow TLS 2024-01-30 01:53:22 +08:00
xream
8a2087c53a feat: 支持更多不规范的 SS URI; 去除 Surfboard 节点名中的等号; 支持 Mihomo shadowsocks shadow-tls 2024-01-30 00:49:41 +08:00
xream
64117c50c7 feat: 支持更多不规范的 VLESS/SS URI 2024-01-29 23:38:21 +08:00
xream
6564d9497a chore: 增加定时任务默认为每天 0 点的说明 2024-01-24 15:17:45 +08:00
xream
1c03e46bbb chore: Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类,参数设置和参数说明(增加双引号 确保兼容性) 2024-01-23 12:45:02 +08:00
xream
084b385fdb chore: Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类,参数设置和参数说明 2024-01-23 04:03:35 +08:00
xream
290b9b5411 chore: 新增 Beta 模块, 支持最新 Surge iOS TestFlight 版本的分类和参数设置 2024-01-22 04:48:34 +08:00
xream
e5c1ae9ed8 feat: 优化流量解析规则 2024-01-20 23:00:37 +08:00
xream
9b6d9d49f9 Merge pull request #275 from dnomd343/master
fix: 流量信息匹配错误
2024-01-20 22:40:13 +08:00
xream
a12adf5255 chore: 脚本操作时不使用空值合并运算符 2024-01-20 22:14:23 +08:00
Dnomd343
8682f14ee7 fix: scientific counting matching error 2024-01-20 13:16:11 +08:00
xream
b3de7a4bc5 feat: 优化调整 Gist 同步逻辑; 增加 GitLab Snippet 同步 2024-01-20 05:33:31 +08:00
xream
099ae5ad83 fix: 配置接口补齐错误处理 2024-01-20 00:50:35 +08:00
xream
c7d00ac512 feat: 域名解析支持类型和过滤 2024-01-19 21:43:54 +08:00
xream
ca0d800bbb release: backend version 2.14.184 2024-01-19 12:54:40 +08:00
xream
31b48d7a6c Merge pull request #274 from izhangxm/feat_add_proxy_convter_api
增加规则转换与协议转换API接口
2024-01-19 12:35:32 +08:00
makabaka
ab96ae9413 增加规则转换与协议转换API接口 2024-01-19 12:23:04 +08:00
xream
3fc507b576 feat: 解析并删除旧的 ws-path ws-headers 字段 2024-01-19 10:18:27 +08:00
xream
2f2dbbdb68 release: backend version 2.14.182 2024-01-18 17:17:15 +08:00
xream
1543e76841 Merge pull request #273 from izhangxm/master
修复clash规则头部有注释的情况下规则转换功能失败的问题
2024-01-18 17:07:59 +08:00
makabaka
74c4719806 fix_clashprovider_test 2024-01-18 15:09:24 +08:00
xream
b80d7f5875 feat: Clash 节点支持 fingerprint(内部转为 tls-fingerprint); 支持 Clash 配置文件中的 global-client-fingerprint 优先级低于 proxy 内的 client-fingerprint 2024-01-18 12:14:35 +08:00
xream
779950ab11 Revert "fix: sing-box fingerprint"
This reverts commit 42404537e8.
2024-01-18 11:36:07 +08:00
xream
42404537e8 fix: sing-box fingerprint 2024-01-18 11:29:15 +08:00
xream
228566116d feat: 支持同步配置时选择包含官方/商店版不支持的协议; 同步配置优化 2024-01-18 06:18:05 +08:00
xream
9bb06bf438 feat: 兼容不规范的 VLESS URI 2024-01-18 01:17:06 +08:00
xream
88e52f9787 revert: 去除 Loon Trojan HTTP 传输层 2024-01-17 22:13:54 +08:00
xream
845a173738 chore: README 2024-01-17 21:55:56 +08:00
xream
4a6bcbc9b4 chore: README 2024-01-17 21:49:56 +08:00
xream
bbaac2de6f fix: Loon 传输层 2024-01-17 21:24:17 +08:00
xream
614438ae3d feat: 支持 QX VLESS 输出(不支持 XTLS/REALITY) 2024-01-17 21:16:34 +08:00
xream
4966132397 feat: produceArtifact 支持 Stash internal (Fixes #271) 2024-01-17 20:31:43 +08:00
xream
059c4bd148 chore: README 2024-01-17 19:55:22 +08:00
xream
63887e3dad feat: 支持解析 QX VLESS 输入; VLESS 无 network 时, 默认为 tcp 2024-01-17 19:30:23 +08:00
xream
7fd585b5d4 feat: SurgeMac 支持 external 2024-01-17 09:15:33 +08:00
xream
16c79ac0fc feat: 支持从 gist 获取不在同步配置中的 gist 文件 2024-01-17 01:10:54 +08:00
xream
14d9885db8 fix: 不上传没有设置来源的同步配置 2024-01-16 23:41:51 +08:00
xream
1e61088ed8 chore: README 2024-01-16 20:52:46 +08:00
xream
af6904ea50 feat: 取消 github 用户名绑定关系(现在用户名错误只影响头像), 增加最近一次 gist 检查状态 2024-01-16 09:44:02 +08:00
xream
1bc44ccde8 feat: 订阅链接可使用标准参数格式 #noCache&noFlow 或 井号附加 #noCache#noFlow 2024-01-16 08:11:34 +08:00
xream
bdc7ee50f7 fix: 修复 sing-box wireguard 输出 2024-01-16 07:24:30 +08:00
xream
812f24d102 feat: 以 #noFlow 结尾的远程链接不查询订阅流量信息 2024-01-16 07:07:55 +08:00
xream
8c943176a5 feat: VLESS URI 输入兼容 Shadowrocket 导出格式 2024-01-16 01:00:22 +08:00
xream
f4c4cdba67 fix: 修复响应头缓存 2024-01-14 23:44:15 +08:00
xream
ada03be05f chore: Loon 插件支持修改响应头缓存有效期 2024-01-14 23:39:27 +08:00
xream
5584225413 feat: 优化订阅流量获取, 启用共享缓存(默认一分钟) 并优先尝试 HEAD 方法 2024-01-14 23:37:55 +08:00
xream
5cbcf4fce4 feat: Node.js 版本体支持定时任务, 环境变量 SUB_STORE_BACKEND_CRON 2024-01-14 18:45:31 +08:00
xream
89931c0032 chore: 文案调整 2024-01-14 15:46:56 +08:00
xream
88f3198320 fix: 找不到资源时报错; 调整脚本操作类型判断; 执行脚本失败时, 同时输出普通脚本和快捷脚本错误 2024-01-14 15:45:08 +08:00
xream
27a14bb255 revert: 回滚文件模板功能 2024-01-14 12:33:36 +08:00
xream
5ecce27f4e feat: 脚本内部 produceArtifact 支持指定 produceType: 'internal', produceOpts: { 'include-unsupported-proxy': true } 来获得内部的数据结构; 订阅链接参数支持 type=internal&includeUnsupportedProxy=true; 文件支持 nunjucks 模板, 为 sing-box 增加的 Filter 用法 sub/col 为订阅/组合订阅中的节点名 {{ '订阅的name' | sub('美国|🇺🇸|us', 'i') }}, subNode/colNode 为订阅/组合订阅中的节点 {{ '订阅的name' | subNode('美国|🇺🇸|us', 'i') }}, 底层 produceArtifact('subscription', 'sing-box', 'internal', '美国|🇺🇸|us', 'i') 2024-01-14 12:13:29 +08:00
xream
12903d77f7 fix: 修复无脚本操作时 文件数据结构错误 2024-01-13 22:01:02 +08:00
xream
0c6ec7f82a revert: 同步接口支持 POST 2024-01-13 20:40:45 +08:00
xream
ba251ced34 fix: 同步接口支持 POST 2024-01-13 19:56:30 +08:00
xream
d96a0421f7 fix: Surge Snell TFO 2024-01-13 16:51:48 +08:00
xream
aff7ddf41e feat: 脚本筛选的快捷操作支持 await 2024-01-13 13:55:07 +08:00
xream
164ae9a7a8 feat: 快捷脚本支持 await; 脚本操作支持 produceArtifact 2024-01-13 13:40:34 +08:00
xream
3aacd26b79 feat: 支持输出到 sing-box; 文件脚本支持 ` ; 脚本支持 ProxyUtils.yaml` 2024-01-13 10:28:07 +08:00
xream
5915416232 feat: 文件支持远程/合并, /api/file/name 接口支持参数覆盖 2024-01-12 07:22:25 +08:00
xream
c059296224 feat: 文件支持脚本操作 2024-01-12 06:16:39 +08:00
xream
9ae70eca09 feat: 同步配置支持文件 2024-01-12 03:52:41 +08:00
xream
d0acf49b83 feat: 文件接口 2024-01-12 02:23:57 +08:00
xream
c51f3511dd fix: 兼容部分不带节点名的 URI 2024-01-08 09:44:53 +08:00
xream
ee2fcc7ee3 fix: 兼容部分不带参数的 URI 输入 2024-01-08 09:28:33 +08:00
xream
95615d1877 feat: 支持全局请求超时(前端 > 2.14.29) 2024-01-08 07:22:03 +08:00
xream
962bcda9dd chore: 同步远程配置输出更多日志 2024-01-07 17:44:03 +08:00
xream
9bb4739d56 Node.js 版的通知支持第三方推送服务. 环境变量名 SUB_STORE_PUSH_SERVICE. 支持 Bark/PushPlus 等服务. 形如: https://api.day.app/XXXXXXXXX/[推送标题]/[推送内容]?group=SubStore&autoCopy=1&isArchive=1&sound=shake&level=timeSensitivehttp://www.pushplus.plus/send?token=XXXXXXXXX&title=[推送标题]&content=[推送内容]&channel=wechat 的 URL, [推送标题][推送内容] 会被自动替换 2024-01-02 22:52:33 +08:00
xream
de1d40f41a feat: Wireguard 结构跟进 Clash.Meta, allowed_ips 改为 allowed-ips 2024-01-02 16:38:48 +08:00
xream
c0ab301160 feat: Trojan URI 支持 gRPC 2023-12-29 16:08:02 +08:00
xream
a22df97a51 release: backend version 2.14.135 2023-12-29 15:42:31 +08:00
xream
45772ade4d Merge pull request #263 from Ariesly/ipv6-uri
fix: Handles node-info IPv6 address URIs
2023-12-29 15:39:03 +08:00
Ariesly
e8dab545f5 fix: Handles node-info IPv6 address URIs 2023-12-29 07:10:47 +00:00
xream
c2bd80207a doc: 补充文档 2023-12-27 02:55:04 +08:00
xream
bc5ae9a2ef feat: 支持 Surfboard(前端 > 2.14.27) 2023-12-27 00:28:15 +08:00
xream
36db057e32 feat: 当节点端口号为合法端口号时, 将类型转为整数(便于脚本判断) 2023-12-23 21:02:39 +08:00
xream
5ac73b863a feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:42:33 +08:00
xream
23042c33d6 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:41:37 +08:00
xream
4ca5f5e355 feat: 支持忽略失败的远程订阅(前端版本 > 2.14.20) 2023-12-18 01:24:48 +08:00
xream
f10e5913fb feat: 兼容部分不规范的机场 Hysteria/Hysteria2 端口跳跃字段为空时 删除此字段 2023-12-17 18:31:12 +08:00
xream
8b75c11587 feat: Hysteria2 URI 输入支持 hy2:// 2023-12-17 16:13:34 +08:00
xream
c287dcad3b fix: 过滤 Stash/Clash Shadowsocks cipher 2023-12-13 20:11:36 +08:00
xream
ce6cd794c8 feat: 环境变量 SUB_STORE_DATA_URL 启动时自动从此地址拉取并恢复数据 2023-12-13 09:54:57 +08:00
xream
e05475aa5e feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 02:04:24 +08:00
xream
c35e9d37ae feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 01:26:16 +08:00
xream
8f2dbfe3df feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_BACKEND_PATH=/prefix 2023-12-13 00:34:08 +08:00
xream
a0a998dfdd feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:11 +08:00
xream
12491ac7c0 feat: Node.js 前端代理后端路由 需设置环境变量 注意安全 SUB_STORE_FRONTEND_PATH=/prefix 2023-12-13 00:26:03 +08:00
xream
78e3024cec feat: Node.js 前端代理后端路由 2023-12-12 22:52:50 +08:00
xream
5e21a20e37 fix: 修复 Loon Trojan WS 传输层 2023-12-12 21:13:17 +08:00
xream
76b5dc5809 feat: 脚本筛选支持节点快捷脚本. 语法与 Shadowrocket 脚本类似
```
const port = Number($server.port)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-22 06:17:45 +00:00
Hsiaoyi
6e9c3ead4c Merge pull request #217 from Keywos/script-Cache
[+] version 2.14.0
2023-07-22 14:15:56 +08:00
K
b3ccd5743a Merge branch 'sub-store-org:master' into script-Cache 2023-07-22 14:13:09 +08:00
K
e18c215fe4 [+] version 2023-07-22 14:11:45 +08:00
Hsiaoyi
e4b54b43a1 Merge pull request #216 from Keywos/script-Cache
script-Cache
2023-07-22 13:56:54 +08:00
K
21726bf950 script-Cache 2023-07-22 13:53:47 +08:00
Hsiaoyi
f6ca9af00f fix: tasks cache in Node.js environment (#209) 2023-05-09 17:16:35 +08:00
Hsiaoyi
39b79b6ca4 feat: Added support for producing Surge nodes with test-url (#199) 2023-03-19 18:32:34 +08:00
NobyDa
999271fa9d Improve resource cache key. (#190) 2023-02-08 12:30:26 +08:00
QuentinHsu
5de35c7720 🐞 fix(subscriptions): negative usage flow (#175) 2022-10-25 00:07:23 +08:00
Jasonzza
06d0c14abc fix: sync artifacts issue (#164) 2022-09-11 23:52:51 +08:00
Peng-YM
029900085c fix: cron-sync-artifacts.js path 2022-09-10 11:47:34 +08:00
Peng-YM
cca5c18d9c build: Update config files 2022-09-10 00:48:11 +08:00
Peng-YM
3de8a1d5be build: Bump version 2022-09-09 23:25:25 +08:00
Peng-YM
94fbe535b3 build: Update GitHub action 2022-09-09 23:15:28 +08:00
Peng-YM
6ee6a9b5e2 build: Split sub-store.min.js for better performance on iOS devices 2022-09-09 23:11:13 +08:00
Peng-YM
1c29771de9 build: Build minimized JavaScripts files 2022-09-09 21:07:28 +08:00
Peng-YM
5845ea1a66 fix (ip-flag.js): Use batch processing to reduce memory footprint 2022-09-09 21:05:05 +08:00
Peng-YM
eb32a09def fix (core): Fix QX vmess-http producer
closes #162
2022-09-09 21:04:08 +08:00
Peng-YM
c60aec603f fix (core): Proxy port smaller that 80 is now allowed
close #167
2022-09-09 21:03:01 +08:00
Peng-YM
0072739f01 refactor (core): Reworked Trojan URI parser to support IPV6 nodes 2022-08-13 12:57:49 +08:00
Peng-YM
1bd2f5f643 fix: xchacha20-ietf-poly1305 cipher is now supported
closes #151
2022-08-13 11:22:23 +08:00
Peng-YM
0a31f5d5d7 fix: ip-flag does not work on Loon
The node parameter should not include proxy name
2022-08-12 00:00:11 +08:00
Peng-YM
6484edb5db feat: Add ip-flag script 2022-08-11 22:58:48 +08:00
Peng-YM
c073870f24 perf: Add support for sending http requests using specific nodes
Only supported on Loon & Surge
2022-08-11 01:07:16 +08:00
Peng-YM
e93332048e fix: Occasional crashed when performing migration 2022-08-10 00:28:46 +08:00
Peng-YM
4dcb9ae79e feat: Include cron-sync-artifact in Stash configuration 2022-08-09 22:42:13 +08:00
Peng-YM
6ea3575101 fix: Rename subscription and collection will break artifacts 2022-08-09 22:28:45 +08:00
Peng-YM
26820ea892 fix: proxy duplicate issue 2022-08-04 20:40:40 +08:00
Peng-YM
f64e8ecfe4 fix: Loon shadowsocksr obfs-param incorrect 2022-08-02 09:23:34 +08:00
Peng-YM
77604a3544 perf (core): DomainResolveProcessor now cache results 2022-07-19 21:29:06 +08:00
Peng-YM
8f5d027080 fix (cron-sync-artifact): sync timeout due to missing await 2022-07-19 20:55:18 +08:00
Peng-YM
5244de4dba fix (config): Sub-Store url is incorrect in Surge.sgmodule 2022-07-13 15:14:11 +08:00
Peng-YM
4121ec2970 chore (gh-action): Trigger workflow run only if package.json have been modified 2022-07-13 15:10:30 +08:00
Peng-YM
a949c49192 perf: Use the latest release scripts in configs 2022-07-13 14:53:44 +08:00
Peng-YM
9fba3506f0 chore: Update GitHub action to automatically release new version 2022-07-13 14:41:34 +08:00
Peng-YM
9677c7ebbd fix (product): cron-sync-artifacts not working 2022-07-13 14:03:43 +08:00
github-actions@github.com
03149dcadb Release 2022-07-13 02:32:52 +00:00
Peng-YM
1bfa6ebb2c fix (core): trojan sni is lost when parsing Clash nodes
#build
2022-07-13 10:31:08 +08:00
Peng-YM
4cd525824e fix (restful): Add query field in IP-API 2022-07-12 23:15:29 +08:00
Peng-YM
813f2b839d perf: Add switch for cron-sync-artifacts 2022-07-12 18:56:24 +08:00
Peng-YM
3d58534dfe fix (restful): Intercept IP-API query failed message when querying node info 2022-07-12 18:34:31 +08:00
Peng-YM
f7333c0617 perf: Include cron script for syncing artifacts in configurations 2022-07-12 18:15:05 +08:00
Peng-YM
f7d4b66db6 feat (restful): Add /api/utils/node-info for querying proxy node info 2022-07-12 15:09:44 +08:00
Peng-YM
8c844eb23a chore: Use pnpm in GitHub action 2022-07-11 23:38:57 +08:00
Peng-YM
de892aaa2b fix: Vmess auto/none cipher parsed incorrectly 2022-07-11 23:33:06 +08:00
Peng-YM
b143476e71 fix (core): Clash Vmess servername does parse correctly 2022-07-11 23:20:21 +08:00
Peng-YM
2c4e47166d feat (restful): Add /api/utils/refresh
The API call does the following:
- Fetch GitHub avatar and update artifact store url
- Revoke all cached resources
2022-07-11 23:06:49 +08:00
Peng-YM
6881148021 perf: Use cache for all remote resources 2022-07-11 20:46:16 +08:00
Peng-YM
848491c0f8 feat: Add support for targetPlatform ShadowRocket 2022-07-11 18:23:56 +08:00
Peng-YM
49c8f2e521 perf: Modify revert.js to completely clear sub-store cache 2022-07-11 18:22:42 +08:00
Peng-YM
2aa3cbd9d2 fix: Backend crash when operator argument is undefined 2022-07-08 16:28:47 +08:00
Peng-YM
d7b97f4595 fix: Error codes are not shown in the response 2022-07-08 11:00:26 +08:00
Peng-YM
8aed0665e3 fix: Restrict artifact name in order to fix a Gist API bug 2022-07-08 10:47:38 +08:00
Peng-YM
b1d811e4c5 fix: Sync time is updated even if upload failed 2022-07-08 10:26:05 +08:00
Peng-YM
2ee515dc23 fix: Full config preprocessor bug 2022-07-07 23:24:01 +08:00
Peng-YM
07cae95ff4 feat: Add artifactStore URL in settings 2022-07-07 22:40:46 +08:00
Peng-YM
240156daef refactor: Revise sync artifacts APIs
- /api/sync/artifacts: sync all artifacts
- /api/sync/artifact/:name: sync a specific artifact
2022-07-07 14:47:09 +08:00
Peng-YM
a420c126c2 refactor: Refactor the arguments of the Flag Operator 2022-07-06 18:12:36 +08:00
Peng-YM
acd0a62496 fix: Set default subscription source to remote when migrating data 2022-07-06 18:12:36 +08:00
Peng-YM
ffd219abfe refactor: Add new frontend as submodule 2022-07-06 18:12:36 +08:00
Peng-YM
bc58419bb1 feat: Added Conditional Filter 2022-07-06 18:12:36 +08:00
Peng-YM
a28c001a5a fix: Useless filter bug 2022-07-06 18:12:36 +08:00
Peng-YM
310d797abf fix: Perform migration after restoring data from gist 2022-07-06 18:12:36 +08:00
Peng-YM
41f84ddb85 chore: nodemon now ignore the changes of sub-store.json 2022-07-06 18:12:36 +08:00
Peng-YM
00c28c6cb8 refactor: Standardize error handling for RESTful APIs 2022-07-06 18:12:36 +08:00
Peng-YM
0e089ef8ce feat: Fetch avatar from GitHub automatically 2022-07-06 18:12:36 +08:00
Peng-YM
7468089f04 fix: QuickSettingOperator does not respect default settings 2022-07-06 18:12:36 +08:00
Peng-YM
b2a797cd25 fix: Useless filters are not migrated 2022-07-06 18:12:36 +08:00
Peng-YM
8e2cd5bb28 fix: Database updateByName not working 2022-07-06 18:12:36 +08:00
Peng-YM
9e33160e37 fix: Process ids are now preserved in migration 2022-07-06 18:12:36 +08:00
Peng-YM
ce51674d6b feat: Add sorting API
Added POST /api/sort/subs, /api/sort/collections, /api/sort/artifacts for sorting subs, collections, and artifacts.
2022-07-06 18:12:36 +08:00
Peng-YM
84b4dba425 refactor: Migrate to API v2
- Added auto schema migration
- Refactored /api/subs, /api/collections, /api/artifacts. Now these APIs will return array instead of object. This enables sorting items in the future.
2022-07-06 18:12:36 +08:00
Peng-YM
b1151859b3 chore: Migrate from npm to pnpm for better performance 2022-07-06 18:12:36 +08:00
Peng-YM
fdbcce8932 doc: Change LICENSE to AGPLv3 2022-07-06 18:11:49 +08:00
github-actions@github.com
297fb1b32a Build sub-store.min.js 2022-07-04 03:47:46 +00:00
Virgil Clyne
0f6a3a8f5f fix (geo.js): adjust the China flag order (#138)
#build
2022-07-04 11:45:47 +08:00
Peng-YM
7834fd6056 fix: Failed to parse Surge/QX nodes with tls fingerprint 2022-07-02 22:53:09 +08:00
Peng-YM
606cfed8da feat (backend): Add preview API
- POST /api/preview/sub for previewing subs
- POST /api/preview/collection for previewing collection
2022-07-01 17:36:48 +08:00
Peng-YM
bb87a6c41e refactor: Download API
Now the download APIs are moved into a new file
2022-06-30 12:19:43 +08:00
Peng-YM
9653b09844 fix: Backend crashes when invalid url is used
Improve error handling for flow info API
2022-06-29 14:07:22 +08:00
Peng-YM
ffc7c07469 perf: Improve /utils/env API
- Add version number
- Add support for Stash & ShadowRocket backend
2022-06-29 11:25:57 +08:00
Peng-YM
7e1139528e feat (backend): Add backend API to get flow info for subscriptions 2022-06-29 00:12:31 +08:00
Peng-YM
79a8956f92 docs: Add version number into products 2022-06-28 13:10:52 +08:00
Peng-YM
03d4d6e918 fix(geo.js): ISO Code at Start/End of String (#137) 2022-06-28 12:52:52 +08:00
github-actions@github.com
f1c8d85ddc fix(geo.js): separate ISO Code from Country Name (#133)
#build
Build sub-store.min.js
2022-06-28 12:44:24 +08:00
Jacob Lee
9202437f05 feat: add preview specific platform feature (#131) 2022-06-25 13:17:12 +08:00
Jacob Lee
013b2173fd Fix: fix the issue that speed dial auto-adaptation theme does not work and is overwritten (#130) 2022-06-24 22:15:06 +08:00
Peng-YM
305ae480bc fix (proxy-parser): ws-headers with multiple keys are not correctly handled when parsing Surge proxies
closes #120
2022-06-24 18:01:31 +08:00
Jacob Lee
730986fed4 feat (UI/sub-editor): Add speed dial for quick saving and adding operations (#129) 2022-06-24 17:22:59 +08:00
QuentinHsu
a1df2de7e2 perf (UI): Use display name in collections
closes #127
2022-06-24 14:41:37 +08:00
Peng-YM
64a453c44a fix (proxy-producer): ws-headers keys are lost when producing Surge nodes
closes #119
2022-06-23 17:59:34 +08:00
Peng-YM
4095606bdf perf: Allow using full configuration as subscription source
Supported configuration formats: Loon, QX, and Surge

closes #121
2022-06-23 15:31:54 +08:00
Hsiaoyi Hsu
f93398b701 feat: Added support for display name 2022-06-22 23:08:15 +08:00
github-actions@github.com
ed86a568ec Build sub-store.min.js 2022-06-22 14:31:15 +00:00
Virgil Clyne
237ecd497e feat: Update getFlag() #build
Update geo.js
2022-06-22 22:29:56 +08:00
Peng-YM
6610f835ba chore: Add dev branch to GitHub action 2022-06-22 22:25:15 +08:00
Peng-YM
ae518a9874 docs: replaced the SIP002 link in commet 2022-06-22 19:25:19 +08:00
Peng-YM
7bd63545cc Update README.md 2022-06-21 22:56:38 +08:00
github-actions@github.com
533b477314 Build sub-store.min.js 2022-06-21 09:04:24 +00:00
Peng-YM
50a3eb5592 fix: unable to delete subscription with trailing spaces #build 2022-06-21 17:02:57 +08:00
Peng-YM
89164c3e7a feat: Added mocha test to GitHub action 2022-06-21 17:01:31 +08:00
Peng-YM
8e0a942a6b fix: vmess aead 2022-06-21 17:00:50 +08:00
Peng-YM
49ed9f1ff3 feat: Added cron script for syncing all artifacts to gist 2022-06-21 16:01:02 +08:00
Peng-YM
94262e162a Added github workflow for building dist files 2022-06-21 15:48:55 +08:00
Peng-YM
05b03b5337 Fix QX SSR issue 2022-06-21 09:24:37 +08:00
Peng-YM
bca4f06fcd feat: Reworked Loon producer 2022-06-20 22:58:27 +08:00
Peng-YM
409c4cbdd9 feat: Added support for parsing Loon vless, Surge socks5 and snell proxies 2022-06-20 22:10:29 +08:00
Peng-YM
d602dbeb7c feat: Added dedicated Stash producer 2022-06-20 21:51:13 +08:00
Peng-YM
bea2c64e40 Fixed min.js not found issue 2022-06-20 20:38:54 +08:00
Peng-YM
99946a8795 Fix trojan URI issues 2022-06-20 19:32:57 +08:00
Peng-YM
e401a31b6c Various bug fixes for URI format parsing 2022-06-20 17:01:09 +08:00
QuentinHsu
4dde556daf feat: add version info in console.log (#117) 2022-06-20 14:14:03 +08:00
Peng-YM
69ccc6bd08 Fixed QX trojan wss issue 2022-06-20 11:11:53 +08:00
Peng-YM
4945826f2d Now an error will be thrown if a required field of a proxy is missing 2022-06-20 01:33:24 +08:00
Peng-YM
83504d42c4 Fixed QX tls issue 2022-06-19 01:14:38 +08:00
Hsiaoyi Hsu
340e3746fe Removed Surge vmess encrypt-method (#116) 2022-06-18 10:15:02 +08:00
Peng-YM
467c03c9d3 Minor bug fix 2022-06-17 22:39:41 +08:00
Peng-YM
8fe645ac3f Set vmess alterId to 0 if not specified 2022-06-17 22:19:02 +08:00
Peng-YM
c12ff37f68 Reworked Surge producer 2022-06-17 22:12:01 +08:00
Peng-YM
0da236fbb1 Minor bug fixes 2022-06-17 20:00:26 +08:00
Peng-YM
1b526414cd Fixed QX vmess method auto 2022-06-16 18:47:40 +08:00
Peng-YM
4b73a1494b Reworked QX producer 2022-06-16 18:35:24 +08:00
Peng-YM
1e22243808 Minor bug fix for chacha20-ietf-poly1305 2022-06-16 16:23:54 +08:00
Peng-YM
ea00b2458c Refactored proxy producers structure 2022-06-16 14:37:51 +08:00
Peng-YM
8fe2280232 Use babel relative import path 2022-06-16 14:24:32 +08:00
Peng-YM
6bfe2700cf Refactored proxy-utils structure 2022-06-16 12:58:24 +08:00
Peng-YM
f91f34d644 Reintroduce storage management API, closes #113 2022-06-16 11:28:15 +08:00
Peng-YM
56ada242d2 Minor refactor 2022-06-16 11:19:07 +08:00
Peng-YM
f76096f244 Removed test.js 2022-06-16 00:55:03 +08:00
Peng-YM
275097f58b Sub-Store 2.0 major release
- Used Peggy.js to replace the original parsers for Loon, QX and Surge.
- Added support for vmess + ws, vmess + http, snell, socks 5 parsing.
- Added various test cases for parsing.
2022-06-16 00:15:03 +08:00
Peng-YM
98bb91babd Hot fix for Surge HTTP 2.0 feature 2022-06-15 23:36:23 +08:00
Peng-YM
ec7c3d8c26 Updated revert.js 2022-06-15 16:45:17 +08:00
Peng-YM
051a01efb0 Hot fix for Surge HTTP 2.0 feature 2022-06-14 23:52:08 +08:00
Peng-YM
97de476ae3 Added global variables for Loon to ScriptOperator and ScriptFilter 2022-06-13 14:21:07 +08:00
NobyDa
9e05c8d7f0 Fix Surge TCP connection shortage. (#110) 2022-06-10 19:29:43 +08:00
Peng-YM
1c5903cda1 Fixed cloud artifact encode issue 2022-06-09 15:53:07 +08:00
Peng-YM
9e50f1b75a Fixed a bug when domain resolve failed, proxy server is missing 2022-06-07 16:56:52 +08:00
Peng-YM
c46c6332e0 Added sans-serif fonts 2022-06-07 16:45:23 +08:00
Peng-YM
7aa3518ce5 Fixed emoji handling in fancy-characters.js 2022-06-07 16:37:10 +08:00
Peng-YM
82ad8a5df8 Added ResolveDomainOperator 2022-06-07 15:55:34 +08:00
Peng-YM
71aaa824ec Update geo.js (#106) 2022-06-04 13:40:55 +08:00
Peng-YM
90e611ceef Refactored ProxyUtils and RuleUtils 2022-06-03 22:51:39 +08:00
Peng-YM
99cc8ce295 Added fancy-characters.js 2022-06-03 20:43:31 +08:00
Peng-YM
1678d2cb65 Replaced eval with Function for security 2022-06-03 20:36:06 +08:00
Peng-YM
cc628788fc Now the "Script Operator" and "Script Filter" support async functions 2022-06-02 23:58:52 +08:00
Peng-YM
072398938f Updated QX configuration 2022-06-02 13:01:58 +08:00
Peng-YM
e28e440dc5 Added support for non-ascii characters in subscriptions, collections and artifacts 2022-05-30 13:47:59 +08:00
Peng-YM
4dbafb77c5 Added deprecation notification for Loon resource parser 2022-05-26 13:49:30 +08:00
Peng-YM
8dbeb8eff3 Fixed Node.js cannot start issue 2022-05-26 11:17:36 +08:00
Peng-YM
a9afae92c5 Fixed Surge https proxy issue 2022-05-25 23:02:06 +08:00
Peng-YM
51db70a1c3 Fixed local subscription not working in collections 2022-05-25 22:33:54 +08:00
Peng-YM
1ac077101d Minor fixes 2022-05-25 18:34:02 +08:00
Peng-YM
e5c256803a Refactored Loon resource parser 2022-05-25 18:17:15 +08:00
Peng-YM
7ed08ef451 Added support for local subscription 2022-05-25 17:32:05 +08:00
Peng-YM
5eceb8d72b Fixed express port issue 2022-05-25 14:42:26 +08:00
Peng-YM
6169ff5255 Refine project structure 2022-05-25 14:33:46 +08:00
Peng-YM
bbe29a7fac Refactored OpenAPI 2022-05-25 11:27:53 +08:00
Peng-YM
0e46d8e14d Minor changes 2022-05-25 11:00:00 +08:00
Peng-YM
c389aa19a2 Updated scripts 2022-05-25 10:33:54 +08:00
Peng-YM
b56f17606e Fixed ESLint issues 2022-05-24 22:24:31 +08:00
Peng-YM
3b70cc0bd8 Added ESLint pipeline into Gulp 2022-05-24 22:19:03 +08:00
Peng-YM
6b22ee7c5d Add nodemon for hot-reloading 2022-05-24 21:35:23 +08:00
Peng-YM
e228416718 Bump to ES6 2022-05-24 21:20:26 +08:00
Peng-YM
46e37df110 Auto darkmode 2022-05-24 17:05:56 +08:00
Peng-YM
975b53db41 Add vmess ws obfs host script 2022-05-24 12:25:14 +08:00
Peng-YM
47451c2df7 Fixed collection ordering is not preserved 2022-05-24 11:31:54 +08:00
Peng-YM
bd58aaa2a4 Fixed UA not working 2022-05-24 10:24:54 +08:00
Peng-YM
52fe634b60 Update README.md 2022-05-23 18:47:34 +08:00
Peng-YM
9216f5c256 Modularized Sub-Store 2022-05-23 18:33:48 +08:00
Peng-YM
3caf743b09 Removed airports configs 2022-05-23 12:24:12 +08:00
qwerzl
7dd8c25663 把澳门加回来 (#96) 2022-05-20 15:30:10 +08:00
qwerzl
65bfcc8a69 国旗列表:添加了一些城市及其英文名 (#95) 2022-05-20 12:15:02 +08:00
Peng-YM
2f183b5644 Merge pull request #93 from qwerzl/master 2022-05-20 08:14:25 +08:00
qwerzl
62b12a0ff1 Added “AE”, ”CA” for Canada and UAE flags 2022-05-20 07:36:39 +08:00
qwerzl
44a4d6a415 Added “AE”, ”CA” for Canada and UAE flags 2022-05-19 23:38:44 +08:00
Peng-YM
8ab2dd17b0 Add logging for remote script 2022-05-18 19:20:13 +08:00
Peng-YM
6bb837f74f Added $argument for "ScriptFilter" and "ScriptOperator", closes #90
Example: https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/scripts/tls-fingerprint.js#fingerprint=67:1B:C8:F2:D4:20:EE:A7:EE:60:DA:BB:A3:F9:A4:D7:C8:29:0F:3E:2F:75:B6:A9:46:88:48:7D:D3:97:7E:98
2022-05-18 13:41:12 +08:00
Peng-YM
48d533af83 Fixed cronSyncArtifact failed issue 2022-05-18 11:06:40 +08:00
Peng-YM
5954ff0a58 Reformat code 2022-05-17 10:24:05 +08:00
Peng-YM
261214aa61 Merge pull request #88 from Marsoit/patch-1 2022-05-16 16:27:13 +08:00
Jupiter
52d73eefc5 Update sub-store.js 2022-05-16 16:16:17 +08:00
Peng-YM
2385027624 Fixed cache bug 2022-05-14 14:37:50 +08:00
Peng-YM
9b1d3d63dd Fixed cronSyncArtifacts error 2022-05-09 21:20:23 +08:00
Peng-YM
93a95b8649 Merge pull request #85 from xaver985/master 2022-05-09 15:11:48 +08:00
Peng-YM
1e1ffd1fca Update Loon.plugin 2022-05-09 11:16:55 +08:00
Xaver Wang
7fee2d50c7 add flags 2022-05-09 02:47:23 +00:00
Xaver Wang
9b3e56079b Update sub-store.js 2022-05-09 10:22:27 +08:00
Peng-YM
6910c7e6e4 Update Loon.plugin 2022-05-05 10:57:56 +08:00
Peng-YM
ef5866d93d Merge pull request #83 from JOJOforshaun/patch-1 2022-05-05 08:54:06 +08:00
JOJOforshaun
76e3e77350 Update README.md
添加小火箭配置说明。
2022-05-05 08:34:20 +08:00
Peng-YM
5035712261 Added support for ShadowRocket, closes #82 2022-05-02 11:07:55 +08:00
Peng-YM
a242fc19df Use cache in cronSyncArtifact to avoid downloading subscription multiple times 2022-05-02 11:07:55 +08:00
Peng-YM
7b68f9fa85 Improve the performance of cronSyncArtifact 2022-04-30 11:29:13 +08:00
Peng-YM
4dc40e92ae Forward subscription-userinfo from the first subscription in a collection 2022-04-28 17:03:01 +08:00
Peng-YM
8b1928e2da Forward subscription-user-info header for Stash 2022-04-28 16:19:53 +08:00
Peng-YM
063cbee198 Fixed targetPlatform parameter not working on collections
Bug fixed
2022-04-18 16:13:37 +08:00
Peng-YM
3ce237d722 Updated tls-fingerprint.js using the new targetPlatform parameter 2022-04-17 23:34:26 +08:00
Peng-YM
f7547bf719 Added an additional parameter targetPlatform for Script Operator 2022-04-17 23:34:26 +08:00
Peng-YM
9f2131551f Update README.md 2022-04-01 20:04:33 +08:00
Peng-YM
892df888cf Depreciated: removed cron-sync-artifacts.js
Synchronizing artifacts through cron script  is not longer supported, please use ShortCut app instead, see #46.
2022-04-01 20:02:07 +08:00
Peng-YM
5a8fb4747d Merge pull request #77 from dompling/master 2022-04-01 19:58:11 +08:00
ShellManager
d65f418e5a 1.修复 mian.js App.vue 引入顺序,导致打包样式错乱问题
2.修复在本地运行或打包时出现 sass 警告
3. 注释了 app.scss 435 行的样式导致按钮背景色显示 #999
2022-03-31 16:14:20 +08:00
Peng-YM
2328b5ac31 Added an example of tls fingerprint configuration 2022-03-29 22:29:03 +08:00
Peng-YM
694a932c57 Fixes tfo issues 2022-03-29 15:30:18 +08:00
Peng-YM
f4ff4d5f97 Added UDP relay support for the Trojan protocol on Surge 2022-03-26 21:49:29 +08:00
Peng-YM
0a96cb9df1 Bug fixes
1. 当 VMess 的 alterId 为 0 时,AEAD 会自动开启。
2. 更新了 Clash 的 VMess Websocket 参数写法,旧版本的写法(如 ws-path, ws-headers 等)已不被支持。
2022-03-25 22:54:42 +08:00
Peng-YM
991bedd663 Add UA auto detection for Stash iOS 2022-03-23 17:36:57 +08:00
Peng-YM
638f4ae8d9 Update README.md 2022-03-22 16:00:07 +08:00
Peng-YM
b034508a86 更新配置文档 2022-03-22 15:58:51 +08:00
Peng-YM
094650dddd Merge pull request #76 from StashNetworks/master
add Stash for iOS
2022-03-22 15:57:57 +08:00
dev
9c60e73531 add Stash for iOS 2022-03-21 22:47:42 +08:00
Peng-YM
093102e791 Add support for Loon UDP relay (SS/SSR/Trojan) 2022-03-11 23:05:20 +08:00
Peng-YM
265a88fc5f Added support for Vmess AEAD (including Loon/QX/Surge), closes 70 2022-03-11 22:00:41 +08:00
Peng-YM
a981763982 Added duplicate alert for collection, closes #74 2022-03-11 21:08:43 +08:00
Peng-YM
2b419ee8e4 Update README.md 2022-01-12 10:20:39 +08:00
Peng-YM
f798bd3aa2 Minified 2021-12-22 18:28:54 +08:00
Peng-YM
090b1cd557 使用 js-yaml 解析 Clash 配置以提升兼容性 2021-12-22 18:23:31 +08:00
Peng-YM
4d1c9a49ac Merge pull request #61 from dompling/master
更新 sub-store.js 兼容 node 服务端
2021-10-24 14:26:38 +08:00
Peng-YM
53b8297e24 Merge branch 'master' into master 2021-10-24 14:26:29 +08:00
Peng-YM
697d9ded8e 新增国旗 (#58) 2021-10-24 14:22:13 +08:00
KotaHv
b24db4a907 修复Loon http https生成相反的问题 (#62) 2021-10-23 21:19:27 +08:00
dompling
6140134c03 nginx 2021-10-19 15:23:31 +08:00
dompling
52ae93dea9 sub-store 2021-10-18 14:40:40 +08:00
dompling
116bdb6354 nginx 服务器配置 2021-10-18 09:29:22 +08:00
dompling
dbe107681d 更新 sub-store.js 兼容 node 服务端 2021-10-18 09:17:48 +08:00
Xaver Wang
1942f6bad8 新增奇奇怪怪国家国旗 2021-10-11 21:47:41 +08:00
Xaver Wang
bfce88ea7a Merge ; commit '638b510c7a812c3d8a79d8295a14c706b7d0d3b2' 2021-10-11 21:36:51 +08:00
Xaver Wang
9ded46c655 Merge branch 'master' of disabled:git@github.com:Peng-YM/Sub-Store.git 2021-10-11 21:36:15 +08:00
Xaver Wang
638b510c7a 微调国旗 (#57) 2021-10-07 10:54:51 +08:00
Xaver Wang
4eadff9e14 update 2021-10-07 09:55:33 +08:00
Xaver Wang
fa30c487ab Merge ; commit '5b1e24a4afdc1ca8d06293c172f7130a1c91a1fc'
Conflicts:
	backend/sub-store.min.js
2021-10-07 09:52:22 +08:00
Peng-YM
5b1e24a4af Added Minified File 2021-09-15 22:37:38 +08:00
Skyxim
6cedde4c4e 订阅新增 User-Agent 选项,允许自定义 UA,默认使用 QuantumultX UA 以获取流量信息 2021-09-15 22:35:00 +08:00
Xaver Wang
bff36d6f68 添加几个国旗 (#54)
Many thanks for @xaver985
2021-09-14 16:58:44 +08:00
Xaver Wang
841ec8ae1b fix 2021-09-14 16:53:04 +08:00
Xaver Wang
ad98661de6 update 2021-09-14 00:53:48 +08:00
Xaver Wang
742ddaf0a1 update 2021-09-14 00:51:16 +08:00
Xaver Wang
f65d27fe44 update 2021-09-14 00:46:55 +08:00
Xaver Wang
8fed0d3b1e update 2021-09-14 00:44:58 +08:00
Xaver Wang
e189e796c9 修复Trojan一处解析错误 2021-09-14 00:42:29 +08:00
Xaver Wang
9129c4f267 Loon.plugin 2021-09-14 00:38:53 +08:00
Peng-YM
0fce1a76e5 Add minified file 2021-09-09 18:35:43 +08:00
Skyxim
a9d8572060 添加从Torjan Uri中获取sni值 (#52)
* 添加从Torjan Uri中获取sni值

感谢 Skyxim <skyxim@users.noreply.github.com>
2021-09-09 18:34:12 +08:00
Peng-YM
87deb369fb 添加 Surge Hybrid 参数支持,添加一键上传所有节点配置到 Gist 2021-09-03 18:05:22 +08:00
Matthew Wo
204e5b73a2 preserve ws-headers for vmess in Surge (#49) 2021-09-03 16:41:07 +08:00
Xaver Wang
47b7ac6267 新增部分国旗
感谢 @xaver985
2021-08-18 16:23:28 +08:00
Peng-YM
9e219029f9 新增节点去重操作,可以对重复节点进行删除或者自动重命名 2021-04-15 17:01:36 +08:00
Peng-YM
69e4ddca60 Reformat codes 2021-04-15 17:00:07 +08:00
Peng-YM
e764f33956 Fixed Typo 2021-03-26 20:06:25 +08:00
Peng-YM
c31034ff29 增加 Loon TCP Fast Open (TFO) 选项 (SS/SSR) 的支持 2021-03-26 19:59:55 +08:00
Peng-YM
51c2ea3dde 增加 Loon UDP relay (SS/SSR) 的支持 2021-03-26 19:21:19 +08:00
Peng-YM
c225751b6f 增加 Loon UDP relay 的支持 2021-03-26 19:17:01 +08:00
Peng-YM
e72745fbce 由于QX脚本不走Rewrite,使用Backend方式触发 2021-02-27 14:47:38 +08:00
Peng-YM
a0691bedaf 多项改进
1. 现在可以编辑远程配置了。
2. 远程配置增加自动同步的选项,配合Cron脚本可以定期同步并上传配置到gist。
2021-02-27 13:00:32 +08:00
Peng-YM
85ee743988 Minor tweaks 2021-01-25 14:23:16 +08:00
Peng-YM
20ea35c9fb Sub-Store现在会为单个订阅转发流量信息 2021-01-25 14:21:16 +08:00
Peng-YM
99b3d44c63 Update README.md 2021-01-21 10:53:48 +08:00
Peng-YM
6c552500c6 节点预览可以预览原始订阅的节点了 2021-01-04 00:03:43 +08:00
Peng-YM
b092d3fd6a 修复Trojan一处解析错误 2020-12-31 14:30:01 +08:00
Peng-YM
76cd097a23 修复端口非443的Troajn节点无法解析的问题 2020-12-25 14:46:48 +08:00
Peng-YM
1f66e110ab Update README.md 2020-12-17 14:52:41 +08:00
Peng-YM
7ebef72955 修复Base64预处理器未能正确处理某些订阅的问题 2020-12-16 17:21:57 +08:00
Peng-YM
ff9586f2bd Fixed a bug when script download failed 2020-12-15 16:26:11 +08:00
Peng-YM
a0d799c832 修复上传时间错误的bug 2020-12-12 17:04:36 +08:00
Peng-YM
433fe5446f 修复了Surge VMess解析WS header错误的问题,感谢@Paldies 2020-12-12 16:45:28 +08:00
Peng-YM
bf8be6e0cb Removed unused log 2020-12-12 16:11:28 +08:00
Peng-YM
944609ec26 Update README.md 2020-12-12 11:54:56 +08:00
Peng-YM
defa6c0e1f Update README.md 2020-12-12 11:53:13 +08:00
Peng-YM
0146990ea9 Update README.md 2020-12-12 11:52:36 +08:00
Peng-YM
cda2c48ea8 Merge remote-tracking branch 'origin/master' 2020-12-12 11:29:47 +08:00
Peng-YM
60647fd5ca 大量UI改进 2020-12-12 11:28:45 +08:00
fossabot
739f85250e Add license scan report and status (#26)
Signed off by: fossabot <badges@fossa.com>
2020-12-11 17:54:09 +08:00
Peng-YM
3f002e0c52 夜间模式 2020-12-11 11:59:02 +08:00
Peng-YM
1f998f70b5 修复正则排序不生效的bug 2020-12-11 10:26:57 +08:00
Peng-YM
fbfc1084fa 修复一处typo 2020-12-10 22:16:55 +08:00
Peng-YM
a6bf4e5c76 正则表达式相关的节点操作支持使用(?i)指定忽略大小写匹配 2020-12-10 21:59:16 +08:00
Peng-YM
7eed0192bc 修复QX无法上传下载gist的bug 2020-12-09 14:14:25 +08:00
Peng-YM
6758f5b6e0 修复QX请求头没有Stringify导致请求超时的问题 2020-12-09 13:12:55 +08:00
Peng-YM
3dddc53ed3 修复列表图标闪烁问题 2020-12-09 11:17:42 +08:00
Peng-YM
5f1b389548 UI改进 2020-12-09 10:04:48 +08:00
Peng-YM
64709b6610 修复UI不刷新的bug 2020-12-09 09:50:55 +08:00
Peng-YM
bb8bac760e Sub-Store 1.2 支持生成远程配置
现在允许用户生成配置并上传到Gist。
2020-12-08 20:19:46 +08:00
Peng-YM
29abac4619 引入Artifacts相关API 2020-12-07 22:01:27 +08:00
Peng-YM
8c22f1c16e 修复正则排序的bug 2020-12-05 15:46:44 +08:00
Peng-YM
9333d989bc Merge branch 'dev' 2020-12-05 13:53:08 +08:00
Peng-YM
2766e23aa0 Sub-Store 1.0版本
1. 移除了所有基于关键词的节点操作,统一使用基于正则表达式的节点操作。
2. UI的大量改进。
2020-12-05 13:39:11 +08:00
Peng-YM
4d1b80ff63 Improved notifications 2020-12-03 15:47:32 +08:00
Peng-YM
f540e49bca Improved log 2020-12-02 16:35:54 +08:00
Peng-YM
f54b87dff3 修正了几个订阅解析错误
- 修复了Quantmult格式VMess的obfs-path和obfs-header解析错误的问题。
- 修复了SSR URI格式protoparams和obfsparams解析错误的问题
2020-12-01 14:55:35 +08:00
Peng-YM
8195fb8235 修正规则解析错误,同时修复一处编码问题 2020-11-27 20:38:55 +08:00
Peng-YM
18164a0c7d 修复分流解析错误的问题 2020-11-27 16:26:41 +08:00
Peng-YM
1398782595 Loon Parser 2020-11-27 11:40:47 +08:00
Peng-YM
eef1559706 修复QX IP-CIDR分流no-resolve解析不正确的问题 2020-11-26 19:59:27 +08:00
Peng-YM
99514f521b 添加一些内置分流 2020-11-26 19:50:54 +08:00
Peng-YM
117a920faf 再次修复之前的bug 2020-11-26 15:24:30 +08:00
Peng-YM
fb7956b589 修复无法下载订阅的bug,我真是bug制造大师... 2020-11-26 15:16:26 +08:00
Peng-YM
2b3f2bbf28 修复一处错误的通知提示 2020-11-26 14:05:47 +08:00
Peng-YM
a6374ac9c3 添加Sub-Store分流支持 2020-11-26 13:43:54 +08:00
Peng-YM
e630caf386 修复备份错误的bug,请重新上传 2020-11-25 12:04:28 +08:00
Peng-YM
0fbdbe7b85 修复组合订阅节点处理错误的问题 2020-11-25 10:47:21 +08:00
Peng-YM
710d5b14e5 修复Loon和Surge上无法保存的bug 2020-11-24 22:59:04 +08:00
Peng-YM
f9dd77ed41 修复无法保存创建订阅的bug 2020-11-24 22:29:23 +08:00
Peng-YM
22c8483f17 紧急修复BUG 2020-11-24 22:11:05 +08:00
Peng-YM
ff4790f12e 组合订阅现在允许添加所有节点操作 2020-11-24 21:21:43 +08:00
Peng-YM
f886bc11e9 完全重构Sub-Store后端代码 2020-11-24 21:15:08 +08:00
Peng-YM
650810bcba 修复QX VMess解析的一处错误 2020-11-09 19:50:02 +08:00
Peng-YM
afa88a79a9 Removed redudant comma 2020-11-02 14:27:23 +08:00
Peng-YM
5be7f2e1ea Add timestamp for minified script 2020-10-27 20:26:21 +08:00
Peng-YM
1ae804ce6b Fixed Loon & Surge Bug 2020-10-27 20:09:48 +08:00
Peng-YM
da71611d70 Fixed Clash TLS host bug 2020-10-20 17:45:21 +08:00
Peng-YM
0d1e0588c5 Fixed some default parameter errors 2020-10-20 17:19:54 +08:00
Peng-YM
fe5dcd2a1b Fixed Clash plugin options parse error 2020-10-20 14:58:07 +08:00
omeks
f3b780370c Fixed QX Producer Bug (#13)
Special thanks to @omeks
2020-10-14 10:27:17 +08:00
Peng-YM
1fb453c106 组合订阅现在可以在链接后添加?target=Clash等指定输出的目标格式了 2020-10-10 16:42:33 +08:00
Peng-YM
4cb8ba1af9 Fixed Loon Bug 2020-10-06 11:30:39 +08:00
Peng-YM
966d1e6ab9 Try to fix loon bug 2020-10-06 10:54:02 +08:00
Peng-YM
22914f792c Add status codes for QX 2020-10-05 22:33:16 +08:00
Peng-YM
915c0d20ae Add status codes 2020-10-05 22:32:49 +08:00
Peng-YM
c619e4e6eb 添加跳转 2020-10-05 22:07:14 +08:00
Peng-YM
a9a68be9f6 Delete backend for QX 2020-10-05 12:12:32 +08:00
Peng-YM
9d97c23186 fixed revert 2020-10-04 20:24:30 +08:00
Peng-YM
ab9343b79a Add 2020-10-04 18:53:34 +08:00
Peng-YM
768b56d7fe 添加急救 2020-10-04 16:34:02 +08:00
Peng-YM
ea7f208882 适配QX TF 390+特性 2020-10-04 10:37:04 +08:00
Peng-YM
77ec9c6953 添加导入数据的接口 2020-10-03 21:32:23 +08:00
Peng-YM
fc27450759 add airport configs database 2020-10-03 21:21:38 +08:00
Peng-YM
4448fc74c3 添加导出所有数据的API接口 2020-10-03 21:06:03 +08:00
Peng-YM
317e3fc724 修复国旗优先级问题 2020-10-02 11:21:04 +08:00
Peng-YM
33ae67ab60 Add Clash and Subscription producer 2020-09-29 17:09:29 +08:00
Peng-YM
8b626ffad3 Add some logs 2020-09-24 15:47:00 +08:00
Peng-YM
4b25874dc6 Minor bug fixed 2020-09-24 15:12:45 +08:00
Peng-YM
3b57b895e2 支持配置Gist同步 2020-09-24 14:59:14 +08:00
Peng-YM
d30ece21ae Fixed single proxy display error 2020-09-23 18:00:04 +08:00
Peng-YM
3d208c9e9f Fixed whitespace issue for QX 2020-09-21 13:45:10 +08:00
Peng-YM
535a20bd90 Add script demo 2020-09-21 13:23:05 +08:00
Peng-YM
d8129de848 Fixed V2RayN SNI issue 2020-09-21 10:15:05 +08:00
Peng-YM
0d3e6bf548 Bug fixed 2020-09-20 10:05:30 +08:00
Peng-YM
ab9d7d6a80 Bug fixed 2020-09-20 10:00:22 +08:00
Peng-YM
fac377735d Added two examples for script operators 2020-09-20 09:56:40 +08:00
Peng-YM
32703deb2c Fixed scert for Surge 2020-09-19 16:59:22 +08:00
Peng-YM
05f746571a 添加导入导出配置功能 2020-09-18 12:56:37 +08:00
Peng-YM
af9f96d101 实现关键词排序UI 2020-09-18 11:07:23 +08:00
Peng-YM
910f2c1b67 提供更方便的脚本操作接口 2020-09-15 11:13:56 +08:00
Peng-YM
a0d8e79d3c Fixed Trojan issue 2020-09-14 10:12:53 +08:00
Peng-YM
8504738ee1 Fixed Trojan server name bug 2020-09-14 09:44:00 +08:00
Peng-YM
1cecbe541e Merge remote-tracking branch 'origin/master' 2020-09-12 22:29:46 +08:00
Peng-YM
893a659bb9 添加Loon新UA判断 2020-09-12 22:29:18 +08:00
Peng-YM
fea3dfb7b9 添加Loon新UA判断 2020-09-12 22:28:11 +08:00
Peng-YM
0842b3fcbc Delete sub.json 2020-09-12 11:17:35 +08:00
Peng-YM
dac2ec95ae Delete collection.json 2020-09-12 11:17:26 +08:00
Peng-YM
92562dff6c Minified backend 2020-09-09 20:30:25 +08:00
Peng-YM
34474c193b Fixed Clash config memory issue 2020-09-09 19:51:04 +08:00
Peng-YM
ad660da24b Update README.md 2020-09-08 12:35:00 +08:00
Peng-YM
786da08b1b Update README.md 2020-09-08 12:32:16 +08:00
Peng-YM
d5d77ea1e6 UPDATE README 2020-09-06 20:57:21 +08:00
Peng-YM
77ee48366e Allow share QR Code 2020-09-06 20:53:11 +08:00
Peng-YM
82fd8cd02c Update README.md 2020-09-06 11:54:27 +08:00
Peng-YM
93953b9c46 Merge pull request #2 from zZPiglet/master
尝试增加 SSD 支持。感谢Z神
2020-09-06 11:54:05 +08:00
Peng-YM
1f2757197a Fixed YAML parse 2020-09-06 11:53:07 +08:00
Peng-YM
e259ed2e6e Supported URI Producer 2020-09-06 11:39:48 +08:00
zZPiglet
4a57b30f71 修正 ssd plugin 错误。 2020-09-06 02:13:03 +08:00
zZPiglet
e25c83368e 修正 localhost。 2020-09-06 01:05:48 +08:00
zZPiglet
568b2a1460 尝试增加 SSD 支持。 2020-09-05 20:07:41 +08:00
Peng-YM
9fcea60555 Bug fixed for Loon 2020-09-05 09:48:39 +08:00
Peng-YM
6b97c4c2d7 Bug fixed 2020-09-04 21:15:26 +08:00
Peng-YM
ff89902733 Add IP_API 2020-09-04 20:53:52 +08:00
Peng-YM
0683230303 Add scripts 2020-09-04 18:30:38 +08:00
Peng-YM
3ed39271b1 Bug Fixed for reording multiple processors 2020-09-04 15:05:25 +08:00
Peng-YM
2e758ec7c8 Set UA to QX 2020-09-03 16:54:05 +08:00
Peng-YM
250afb10a2 Added script filter and script opeartor 2020-09-03 16:16:28 +08:00
Peng-YM
82a8c16e51 Bug fixed for keyword replace 2020-09-03 12:54:34 +08:00
Peng-YM
1ea5418871 Fixed Canada flag 2020-09-03 10:30:07 +08:00
Peng-YM
1c06bd8869 UI Improvements 2020-09-03 10:23:50 +08:00
Peng-YM
80294618fb Minor Bug fixed 2020-09-01 20:24:32 +08:00
Peng-YM
2d6aa1ef1a bug fixed 2020-09-01 17:56:24 +08:00
Peng-YM
8fc5e58276 修复缓存 2020-09-01 16:18:59 +08:00
Peng-YM
56b350a649 启用订阅缓存,大幅减少浏览器响应延迟 2020-09-01 12:08:14 +08:00
Peng-YM
b2f9eb9989 添加四个重命名 2020-09-01 10:11:02 +08:00
Peng-YM
4873c6bb3c 修复过滤器切换模式不生效的bug 2020-09-01 08:43:30 +08:00
Peng-YM
1fd3326c5c ... 2020-09-01 03:27:54 +08:00
Peng-YM
d4dcc5a906 Update RegionFilter.vue 2020-09-01 01:59:36 +08:00
Peng-YM
af0baba727 修复过滤无效节点的bug 2020-09-01 00:39:15 +08:00
Peng-YM
089b36c4fe 添加正则过滤器,关键词过滤器 2020-08-31 23:57:34 +08:00
Peng-YM
ea90f16e54 UI添加两个过滤器: 区域过滤器和类型过滤器 2020-08-31 21:10:36 +08:00
Peng-YM
9291c4f628 Update README.md 2020-08-31 19:19:59 +08:00
Peng-YM
78991ddca8 修复SS URI的一处解析错误 2020-08-31 19:10:29 +08:00
Peng-YM
8a85d6dd21 添加Quantumult格式的VMess订阅支持 2020-08-31 17:16:27 +08:00
Peng-YM
23b4532037 修改UA为Quantumult,以获取流量信息等 2020-08-31 15:50:54 +08:00
Peng-YM
d81fd8cb62 修改UA为Quantumult,以获取流量信息等 2020-08-31 15:50:34 +08:00
Peng-YM
00be2678f4 修复SS无混淆插件的情况 2020-08-31 15:09:53 +08:00
Peng-YM
c5b19f5286 Merge remote-tracking branch 'origin/master' 2020-08-31 14:53:34 +08:00
Peng-YM
7495ba6f78 修复obfs-local格式SS订阅 2020-08-31 14:53:13 +08:00
Peng-YM
85e3801a26 Update Surge.sgmodule 2020-08-31 14:05:53 +08:00
Peng-YM
2a04476bca 处理Option请求 2020-08-31 14:02:56 +08:00
Peng-YM
a4f096b471 Surge iOS已修复,感谢@pysta大佬协助 2020-08-31 13:44:00 +08:00
Peng-YM
49349365fa Adapt status code 2020-08-31 13:41:39 +08:00
Peng-YM
a9811c4896 Try to fix QX 2020-08-31 13:09:13 +08:00
Peng-YM
5a2a600c38 Merge remote-tracking branch 'origin/master' 2020-08-31 12:27:05 +08:00
Peng-YM
f007bc7c23 修复过滤器逻辑错误 2020-08-31 12:26:30 +08:00
Peng-YM
c4fd5d363b Update sub-store.js 2020-08-31 03:27:27 +08:00
Peng-YM
231577eebd Update Surge.sgmodule 2020-08-31 02:50:33 +08:00
Peng-YM
88634b146a Merge ; commit '47d06919aa1524905cb68fcab67f5b8968e5d0d1'
Conflicts:
	config/Surge.sgmodule
2020-08-31 02:44:46 +08:00
Peng-YM
fa8b5b50a2 Update Surge.sgmodule 2020-08-31 02:42:15 +08:00
Peng-YM
47d06919aa Update Surge.sgmodule 2020-08-31 02:19:04 +08:00
Peng-YM
bd6d0849e4 Merge remote-tracking branch 'origin/master' 2020-08-31 02:15:43 +08:00
Peng-YM
e36c719ef0 Change Domain 2020-08-31 02:14:45 +08:00
Peng-YM
1b4966620c Update Surge.sgmodule 2020-08-31 02:05:39 +08:00
Peng-YM
c75d318961 修改域名为sub.com 2020-08-31 01:08:49 +08:00
Peng-YM
ae7953a0eb 更换域名 2020-08-31 00:59:47 +08:00
Peng-YM
ad18b11d56 QX header修复 2020-08-31 00:57:06 +08:00
Peng-YM
1a3d1ac1c0 [Bug Fixed] 修复Loon VMess错误 2020-08-31 00:37:11 +08:00
Peng-YM
f9a1da4b27 Update README.md 2020-08-31 00:28:00 +08:00
Peng-YM
b7a140e8fd Update README.md 2020-08-31 00:27:48 +08:00
Peng-YM
bff31187d8 [Bug Fixed] 修复Loon VMess错误 2020-08-31 00:15:48 +08:00
Peng-YM
d14bd41b68 Add more timeout to axios 2020-08-30 21:58:31 +08:00
Peng-YM
3d2cd1569f Add config for Surge and Loon 2020-08-30 21:52:17 +08:00
Peng-YM
03bc695ae3 Bug fixed 2020-08-30 21:45:31 +08:00
Peng-YM
583f94e5b3 Alpha test 2020-08-30 21:25:46 +08:00
Peng-YM
a71914676e Working on sub-editor 2020-08-28 15:00:45 +08:00
Peng-YM
8b4c972f77 适配移动端web app
样式来自Chavyleung
2020-08-26 21:33:00 +08:00
Peng-YM
aee89acfe8 Merge remote-tracking branch 'origin/master' 2020-08-26 21:10:25 +08:00
Peng-YM
1fcc1f77d7 Add cache 2020-08-26 21:09:34 +08:00
Peng-YM
40cdd7be47 Update README.md 2020-08-25 15:15:01 +08:00
Peng-YM
ccafb9c75e Update README.md 2020-08-25 11:25:34 +08:00
Peng-YM
f279c13239 Update README.md 2020-08-25 11:21:52 +08:00
Peng-YM
1b0f8f124e 支持Clash YAML输入 2020-08-25 11:18:35 +08:00
Peng-YM
8cec472812 Add some components 2020-08-23 12:25:02 +08:00
Peng-YM
0a11261eee Added subscription page 2020-08-23 00:56:35 +08:00
Peng-YM
72bb2f4edb Update README.md 2020-08-21 18:12:29 +08:00
Peng-YM
343692db86 Support remote script 2020-08-21 18:06:27 +08:00
Peng-YM
a610346c5d Merge branch 'master' of https://github.com/Peng-YM/Sub-Store into master 2020-08-19 21:34:44 +08:00
Peng-YM
9aec501a48 Web 2020-08-19 21:34:35 +08:00
Peng-YM
83070fc708 Update README.md 2020-08-19 19:31:15 +08:00
Peng-YM
82690b1e5e Update README.md 2020-08-19 19:30:30 +08:00
Peng-YM
997ef25e2e Added vuetify 2020-08-19 16:36:01 +08:00
Peng-YM
64e11e02e7 Re-organized project structure 2020-08-19 16:18:48 +08:00
Peng-YM
7aa0fa6911 Create LICENSE 2020-08-19 16:16:07 +08:00
80 changed files with 11131 additions and 14870 deletions

View File

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

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
.DS_Store
# json config
sub-store.json
sub-store_*.json
root.json
# Logs

0
.gitmodules vendored Normal file
View File

View File

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

View File

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

View File

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

View File

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

18525
backend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -5,119 +5,14 @@ import {
isPresent,
isNotBlank,
getIfPresent,
getRandomPort,
} from '@/utils';
import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx';
import getTrojanURIParser from './peggy/trojan-uri';
import $ from '@/core/app';
import JSON5 from 'json5';
import YAML from '@/utils/yaml';
import { Base64 } from 'js-base64';
function surge_port_hopping(raw) {
const [parts, port_hopping] =
raw.match(
/,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
) || [];
return {
port_hopping: port_hopping
? port_hopping.replace(/;/g, ',')
: undefined,
line: parts ? raw.replace(parts, '') : raw,
};
}
function URI_PROXY() {
// socks5+tls
// socks5
// http, https(可以这么写)
const name = 'URI PROXY Parser';
const test = (line) => {
return /^(socks5\+tls|socks5|http|https):\/\//.test(line);
};
const parse = (line) => {
// parse url
// eslint-disable-next-line no-unused-vars
let [__, type, tls, username, password, server, port, query, name] =
line.match(
/^(socks5|http|http)(\+tls|s)?:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
if (tls) {
port = 443;
} else if (type === 'http') {
port = 80;
} else {
$.error(`port is not present in line: ${line}`);
throw new Error(`port is not present in line: ${line}`);
}
$.info(`port is not present in line: ${line}, set to ${port}`);
}
const proxy = {
name:
name != null
? decodeURIComponent(name)
: `${type} ${server}:${port}`,
type,
tls: tls ? true : false,
server,
port,
username:
username != null ? decodeURIComponent(username) : undefined,
password:
password != null ? decodeURIComponent(password) : undefined,
};
return proxy;
};
return { name, test, parse };
}
function URI_SOCKS() {
const name = 'URI SOCKS Parser';
const test = (line) => {
return /^socks:\/\//.test(line);
};
const parse = (line) => {
// parse url
// eslint-disable-next-line no-unused-vars
let [__, type, auth, server, port, query, name] = line.match(
/^(socks)?:\/\/(?:(.*)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${line}`);
throw new Error(`port is not present in line: ${line}`);
}
let username, password;
if (auth) {
const parsed = Base64.decode(decodeURIComponent(auth)).split(':');
username = parsed[0];
password = parsed[1];
}
const proxy = {
name:
name != null
? decodeURIComponent(name)
: `${type} ${server}:${port}`,
type: 'socks5',
server,
port,
username,
password,
};
return proxy;
};
return { name, test, parse };
}
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() {
@@ -130,22 +25,14 @@ function URI_SS() {
// parse url
let content = line.split('ss://')[1];
let name = line.split('#')[1];
const proxy = {
name: decodeURIComponent(line.split('#')[1]),
type: 'ss',
};
content = content.split('#')[0]; // strip proxy name
// handle IPV4 and IPV6
let serverAndPortArray = content.match(/@([^/?]*)(\/|\?|$)/);
let rawUserInfoStr = decodeURIComponent(content.split('@')[0]); // 其实应该分隔之后, 用户名和密码再 decodeURIComponent. 但是问题不大
let userInfoStr;
if (rawUserInfoStr?.startsWith('2022-blake3-')) {
userInfoStr = rawUserInfoStr;
} else {
userInfoStr = Base64.decode(rawUserInfoStr);
}
let serverAndPortArray = content.match(/@([^/]*)(\/|$)/);
let userInfoStr = Base64.decode(content.split('@')[0]);
let query = '';
if (!serverAndPortArray) {
if (content.includes('?')) {
@@ -154,7 +41,6 @@ function URI_SS() {
query = parsed[2];
}
content = Base64.decode(content);
if (query) {
if (/(&|\?)v2ray-plugin=/.test(query)) {
const parsed = query.match(/(&|\?)v2ray-plugin=(.*?)(&|$)/);
@@ -168,35 +54,26 @@ function URI_SS() {
}
content = `${content}${query}`;
}
userInfoStr = content.match(/(^.*)@/)?.[1];
serverAndPortArray = content.match(/@([^/@]*)(\/|$)/);
} else if (content.includes('?')) {
const parsed = content.match(/(\?.*)$/);
query = parsed[1];
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)}`.match(
/\d+/,
)?.[0];
let userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = userInfo?.[1];
proxy.password = userInfo?.[2];
// if (!proxy.cipher || !proxy.password) {
// userInfo = rawUserInfoStr.match(/(^.*?):(.*$)/);
// proxy.cipher = userInfo?.[1];
// proxy.password = userInfo?.[2];
// }
const userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = userInfo[1];
proxy.password = userInfo[2];
// handle obfs
const pluginMatch = content.match(/[?&]plugin=([^&]+)/);
const shadowTlsMatch = content.match(/[?&]shadow-tls=([^&]+)/);
if (pluginMatch) {
const idx = content.indexOf('?plugin=');
if (idx !== -1) {
const pluginInfo = (
'plugin=' + decodeURIComponent(pluginMatch[1])
'plugin=' +
decodeURIComponent(content.split('?plugin=')[1].split('&')[0])
).split(';');
const params = {};
for (const item of pluginInfo) {
@@ -221,51 +98,18 @@ function URI_SS() {
tls: getIfPresent(params.tls),
};
break;
case 'shadow-tls': {
proxy.plugin = 'shadow-tls';
const version = getIfNotBlank(params['version']);
proxy['plugin-opts'] = {
host: getIfNotBlank(params['host']),
password: getIfNotBlank(params['password']),
version: version ? parseInt(version, 10) : undefined,
};
break;
}
default:
throw new Error(
`Unsupported plugin option: ${params.plugin}`,
);
}
}
// Shadowrocket
if (shadowTlsMatch) {
const params = JSON.parse(Base64.decode(shadowTlsMatch[1]));
const version = getIfNotBlank(params['version']);
const address = getIfNotBlank(params['address']);
const port = getIfNotBlank(params['port']);
proxy.plugin = 'shadow-tls';
proxy['plugin-opts'] = {
host: getIfNotBlank(params['host']),
password: getIfNotBlank(params['password']),
version: version ? parseInt(version, 10) : undefined,
};
if (address) {
proxy.server = address;
}
if (port) {
proxy.port = parseInt(port, 10);
}
}
if (/(&|\?)uot=(1|true)/i.test(query)) {
proxy['udp-over-tcp'] = true;
}
if (/(&|\?)tfo=(1|true)/i.test(query)) {
proxy.tfo = true;
}
if (name != null) {
name = decodeURIComponent(name);
}
proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`;
return proxy;
};
return { name, test, parse };
@@ -314,7 +158,7 @@ function URI_SSR() {
for (const item of line) {
let [key, val] = item.split('=');
val = val.trim();
if (val.length > 0 && val !== '(null)') {
if (val.length > 0) {
other_params[key] = val;
}
}
@@ -348,7 +192,7 @@ function URI_VMess() {
};
const parse = (line) => {
line = line.split('vmess://')[1];
let content = Base64.decode(line.replace(/\?.*?$/, ''));
let content = Base64.decode(line);
if (/=\s*vmess/.test(content)) {
// Quantumult VMess URI format
const partitions = content.split(',').map((p) => p.trim());
@@ -441,16 +285,7 @@ function URI_VMess() {
type: 'vmess',
server,
port,
// https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link
// https://github.com/XTLS/Xray-core/issues/91
cipher: [
'auto',
'aes-128-gcm',
'chacha20-poly1305',
'none',
].includes(params.scy)
? params.scy
: 'auto',
cipher: getIfPresent(params.scy, 'auto'),
uuid: params.id,
alterId: parseInt(
getIfPresent(params.aid ?? params.alterId, 0),
@@ -461,44 +296,28 @@ function URI_VMess() {
? !params.verify_cert
: undefined,
};
if (!proxy['skip-cert-verify'] && isPresent(params.allowInsecure)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(
params.allowInsecure,
);
}
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
if (proxy.tls) {
if (params.sni && params.sni !== '') {
proxy.sni = params.sni;
} else if (params.peer && params.peer !== '') {
proxy.sni = params.peer;
}
if (proxy.tls && proxy.sni) {
proxy.sni = params.sni;
}
let httpupgrade = false;
// handle obfs
if (params.net === 'ws' || params.obfs === 'websocket') {
proxy.network = 'ws';
} else if (
['http'].includes(params.net) ||
['http'].includes(params.obfs) ||
['http'].includes(params.type)
['tcp', 'http'].includes(params.net) ||
params.obfs === 'http'
) {
proxy.network = 'http';
} else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
proxy.network = params.net;
} else if (['grpc'].includes(params.net)) {
proxy.network = 'grpc';
} else if (
params.net === 'httpupgrade' ||
proxy.network === 'httpupgrade'
) {
proxy.network = 'ws';
httpupgrade = true;
} else if (params.net === 'h2' || proxy.network === 'h2') {
proxy.network = 'h2';
}
// 暂不支持 tcp + host + path
// else if (params.net === 'tcp' || proxy.network === 'tcp') {
// proxy.network = 'tcp';
// }
if (proxy.network) {
let transportHost = params.host ?? params.obfsParam;
try {
@@ -511,17 +330,8 @@ function URI_VMess() {
} catch (e) {}
let transportPath = params.path;
// 补上默认 path
if (['ws'].includes(proxy.network)) {
transportPath = transportPath || '/';
}
if (proxy.network === 'http') {
if (transportHost) {
// 1)http(tcp)->host中间逗号(,)隔开
transportHost = transportHost
.split(',')
.map((i) => i.trim());
transportHost = Array.isArray(transportHost)
? transportHost[0]
: transportHost;
@@ -530,32 +340,13 @@ function URI_VMess() {
transportPath = Array.isArray(transportPath)
? transportPath[0]
: transportPath;
} else {
transportPath = '/';
}
}
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
if (
transportPath ||
transportHost ||
['kcp', 'quic'].includes(proxy.network)
) {
if (transportPath || transportHost) {
if (['grpc'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
'grpc-service-name': getIfNotBlank(transportPath),
'_grpc-type': getIfNotBlank(params.type),
'_grpc-authority': getIfNotBlank(params.authority),
};
} else if (['kcp', 'quic'].includes(proxy.network)) {
proxy[`${proxy.network}-opts`] = {
[`_${proxy.network}-type`]: getIfNotBlank(
params.type,
),
[`_${proxy.network}-host`]: getIfNotBlank(
getIfNotBlank(transportHost),
),
[`_${proxy.network}-path`]:
getIfNotBlank(transportPath),
};
} else {
const opts = {
@@ -571,13 +362,13 @@ function URI_VMess() {
} else {
delete proxy.network;
}
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L413
// sni 优先级应高于 host
if (proxy.tls && !proxy.sni && transportHost) {
proxy.sni = transportHost;
}
}
proxy['client-fingerprint'] = params.fp;
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
// 然而 wiki 和 app 实测中都没有字段表示这个
// proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
return proxy;
}
};
@@ -661,9 +452,6 @@ function URI_VLESS() {
if (params.sid) {
opts['short-id'] = params.sid;
}
if (params.spx) {
opts['_spider-x'] = params.spx;
}
if (Object.keys(opts).length > 0) {
// proxy[`${params.security}-opts`] = opts;
proxy[`${params.security}-opts`] = opts;
@@ -679,9 +467,6 @@ function URI_VLESS() {
}
if (!proxy.network && isShadowrocket && params.obfs) {
proxy.network = params.obfs;
if (['none'].includes(proxy.network)) {
proxy.network = 'tcp';
}
}
if (['websocket'].includes(proxy.network)) {
proxy.network = 'ws';
@@ -703,9 +488,6 @@ function URI_VLESS() {
}
if (params.serviceName) {
opts[`${proxy.network}-service-name`] = params.serviceName;
if (['grpc'].includes(proxy.network) && params.authority) {
opts['_grpc-authority'] = params.authority;
}
} else if (isShadowrocket && params.path) {
if (!['ws', 'http', 'h2'].includes(proxy.network)) {
opts[`${proxy.network}-service-name`] = params.path;
@@ -726,68 +508,14 @@ function URI_VLESS() {
if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts;
}
if (proxy.network === 'kcp') {
// mKCP 种子。省略时不使用种子,但不可以为空字符串。建议 mKCP 用户使用 seed。
if (params.seed) {
proxy.seed = params.seed;
}
// mKCP 的伪装头部类型。当前可选值有 none / srtp / utp / wechat-video / dtls / wireguard。省略时默认值为 none即不使用伪装头部但不可以为空字符串。
proxy.headerType = params.headerType || 'none';
}
if (params.mode) {
proxy._mode = params.mode;
}
if (params.extra) {
proxy._extra = params.extra;
}
}
return proxy;
};
return { name, test, parse };
}
function URI_AnyTLS() {
const name = 'URI AnyTLS Parser';
const test = (line) => {
return /^anytls:\/\//.test(line);
};
const parse = (line) => {
line = line.split(/anytls:\/\//)[1];
// eslint-disable-next-line no-unused-vars
let [__, password, server, port, addons = '', name] =
/^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
password = decodeURIComponent(password);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
password = decodeURIComponent(password);
if (name != null) {
name = decodeURIComponent(name);
}
name = name ?? `AnyTLS ${server}:${port}`;
const proxy = {
type: 'anytls',
name,
server,
port,
password,
};
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/g, '-');
value = decodeURIComponent(value);
if (['alpn'].includes(key)) {
proxy[key] = value ? value.split(',') : undefined;
} else if (['insecure'].includes(key)) {
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(value);
} else if (['udp'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else {
proxy[key] = value;
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;
}
}
@@ -802,43 +530,13 @@ function URI_Hysteria2() {
};
const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2];
// 端口跳跃有两种写法:
// 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
// 端口部分支持 端口跳跃 的「多端口地址格式」。
// https://hysteria.network/zh/docs/advanced/Port-Hopping
// 2. 参数 mport
let ports;
/* eslint-disable no-unused-vars */
let [
__,
password,
server,
___,
port,
____,
_____,
______,
_______,
________,
addons = '',
name,
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
line,
);
/* eslint-enable no-unused-vars */
if (/^\d+$/.test(port)) {
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
} else if (port) {
ports = port;
port = getRandomPort(ports);
} else {
// 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);
@@ -850,7 +548,6 @@ function URI_Hysteria2() {
name,
server,
port,
ports,
password,
};
@@ -869,23 +566,12 @@ function URI_Hysteria2() {
if (params.obfs && params.obfs !== 'none') {
proxy.obfs = params.obfs;
}
if (params.mport) {
proxy.ports = params.mport;
}
proxy.ports = params.mport;
proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
proxy['tls-fingerprint'] = params.pinSHA256;
let hop_interval = params['hop-interval'] || params['hop_interval'];
if (/^\d+$/.test(hop_interval)) {
proxy['hop-interval'] = parseInt(`${hop_interval}`, 10);
}
let keepalive = params['keepalive'];
if (/^\d+$/.test(keepalive)) {
proxy['keepalive'] = parseInt(`${keepalive}`, 10);
}
return proxy;
};
@@ -968,11 +654,8 @@ function URI_TUIC() {
const parse = (line) => {
line = line.split(/tuic:\/\//)[1];
// eslint-disable-next-line no-unused-vars
let [__, auth, server, port, addons = '', name] =
/^(.*?)@(.*?)(?::(\d+))?\/?(?:\?(.*?))?(?:#(.*?))?$/.exec(line);
auth = decodeURIComponent(auth);
let [uuid, ...passwordParts] = auth.split(':');
let password = passwordParts.join(':');
let [__, uuid, password, server, ___, port, ____, addons = '', name] =
/^(.*?):(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
@@ -994,19 +677,14 @@ function URI_TUIC() {
for (const addon of addons.split('&')) {
let [key, value] = addon.split('=');
key = key.replace(/_/g, '-');
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 (['fast-open'].includes(key)) {
proxy.tfo = true;
} else if (['disable-sni', 'reduce-rtt'].includes(key)) {
proxy[key] = /(TRUE)|1/i.test(value);
} else if (key === 'congestion-control') {
proxy['congestion-controller'] = value;
delete proxy[key];
} else {
proxy[key] = value;
}
@@ -1108,11 +786,6 @@ function URI_Trojan() {
};
const parse = (line) => {
const matched = /^(trojan:\/\/.*?@.*?)(:(\d+))?\/?(\?.*?)?$/.exec(line);
const port = matched?.[2];
if (!port) {
line = line.replace(matched[1], `${matched[1]}:443`);
}
let [newLine, name] = line.split(/#(.+)/, 2);
const parser = getTrojanURIParser();
const proxy = parser.parse(newLine);
@@ -1131,26 +804,17 @@ function URI_Trojan() {
function Clash_All() {
const name = 'Clash Parser';
const test = (line) => {
let proxy;
try {
proxy = JSON5.parse(line);
JSON.parse(line);
} catch (e) {
proxy = YAML.parse(line);
return false;
}
return !!proxy?.type;
return true;
};
const parse = (line) => {
let proxy;
try {
proxy = JSON5.parse(line);
} catch (e) {
proxy = YAML.parse(line);
}
const proxy = JSON.parse(line);
if (
![
'anytls',
'mieru',
'juicity',
'ss',
'ssr',
'vmess',
@@ -1164,7 +828,6 @@ function Clash_All() {
'hysteria2',
'wireguard',
'ssh',
'direct',
].includes(proxy.type)
) {
throw new Error(
@@ -1176,10 +839,18 @@ function Clash_All() {
if (['vmess', 'vless'].includes(proxy.type)) {
proxy.sni = proxy.servername;
delete proxy.servername;
if (proxy.tls && !proxy.sni) {
if (proxy.network === 'ws') {
proxy.sni = proxy['ws-opts']?.headers?.Host;
} else if (proxy.network === 'http') {
let httpHost = proxy['http-opts']?.headers?.Host;
proxy.sni = Array.isArray(httpHost)
? httpHost[0]
: httpHost;
}
}
}
if (proxy['server-cert-fingerprint']) {
proxy['tls-fingerprint'] = proxy['server-cert-fingerprint'];
}
if (proxy.fingerprint) {
proxy['tls-fingerprint'] = proxy.fingerprint;
}
@@ -1463,14 +1134,6 @@ function Loon_WireGuard() {
return { name, test, parse };
}
function Surge_Direct() {
const name = 'Surge Direct Parser';
const test = (line) => {
return /^.*=\s*direct/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_SSH() {
const name = 'Surge SSH Parser';
const test = (line) => {
@@ -1614,12 +1277,7 @@ function Surge_Tuic() {
const test = (line) => {
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
};
const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
function Surge_WireGuard() {
@@ -1636,12 +1294,7 @@ function Surge_Hysteria2() {
const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
};
const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
}
@@ -1650,8 +1303,6 @@ function isIP(ip) {
}
export default [
URI_PROXY(),
URI_SOCKS(),
URI_SS(),
URI_SSR(),
URI_VMess(),
@@ -1661,9 +1312,7 @@ export default [
URI_Hysteria(),
URI_Hysteria2(),
URI_Trojan(),
URI_AnyTLS(),
Clash_All(),
Surge_Direct(),
Surge_SSH(),
Surge_SS(),
Surge_VMess(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,32 +2,8 @@ import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
import { isIPv4, isIPv6 } from '@/utils';
const ipVersions = {
ipv4: 'ipv4_only',
ipv6: 'ipv6_only',
'v4-only': 'ipv4_only',
'v6-only': 'ipv6_only',
'ipv4-prefer': 'prefer_ipv4',
'ipv6-prefer': 'prefer_ipv6',
'prefer-v4': 'prefer_ipv4',
'prefer-v6': 'prefer_ipv6',
};
const ipVersionParser = (proxy, parsedProxy) => {
const strategy = ipVersions[proxy['ip-version']];
if (proxy._dns_server && strategy) {
parsedProxy.domain_resolver = {
server: proxy._dns_server,
strategy,
};
}
};
const detourParser = (proxy, parsedProxy) => {
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
};
const networkParser = (proxy, parsedProxy) => {
if (['tcp', 'udp'].includes(proxy._network))
parsedProxy.network = proxy._network;
if (proxy['dialer-proxy']) parsedProxy.detour = proxy['dialer-proxy'];
};
const tfoParser = (proxy, parsedProxy) => {
parsedProxy.tcp_fast_open = false;
@@ -51,34 +27,12 @@ const smuxParser = (smux, proxy) => {
if (smux['min-streams'])
proxy.multiplex.min_streams = parseInt(`${smux['min-streams']}`, 10);
if (smux.padding) proxy.multiplex.padding = true;
if (smux['brutal-opts']?.up || smux['brutal-opts']?.down) {
proxy.multiplex.brutal = {
enabled: true,
};
if (smux['brutal-opts']?.up)
proxy.multiplex.brutal.up_mbps = parseInt(
`${smux['brutal-opts']?.up}`,
10,
);
if (smux['brutal-opts']?.down)
proxy.multiplex.brutal.down_mbps = parseInt(
`${smux['brutal-opts']?.down}`,
10,
);
}
};
const wsParser = (proxy, parsedProxy) => {
const transport = { type: 'ws', headers: {} };
if (proxy['ws-opts']) {
const {
path: wsPath = '',
headers: wsHeaders = {},
'max-early-data': max_early_data,
'early-data-header-name': early_data_header_name,
} = proxy['ws-opts'];
transport.early_data_header_name = early_data_header_name;
transport.max_early_data = parseInt(max_early_data, 10);
const { path: wsPath = '', headers: wsHeaders = {} } = proxy['ws-opts'];
if (wsPath !== '') transport.path = `${wsPath}`;
if (Object.keys(wsHeaders).length > 0) {
const headers = {};
@@ -181,7 +135,7 @@ const h1Parser = (proxy, parsedProxy) => {
host = `${host}`.split(',').map((i) => i.trim());
if (host.length > 0) transport.host = host;
}
// if (!transport.host) return;
if (!transport.host) return;
if (proxy['http-path'] && proxy['http-path'] !== '') {
const path = proxy['http-path'];
if (Array.isArray(path)) {
@@ -190,7 +144,7 @@ const h1Parser = (proxy, parsedProxy) => {
}
if (parsedProxy.tls.insecure)
parsedProxy.tls.server_name = transport.host[0];
if (transport.host?.length === 1) transport.host = 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];
@@ -248,8 +202,13 @@ const tlsParser = (proxy, parsedProxy) => {
parsedProxy.tls.alpn = [proxy.alpn];
} else if (Array.isArray(proxy.alpn)) parsedProxy.tls.alpn = proxy.alpn;
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
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'])
@@ -258,17 +217,7 @@ const tlsParser = (proxy, parsedProxy) => {
if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id'];
parsedProxy.tls.utls = { enabled: true };
}
if (
!['hysteria', 'hysteria2', 'tuic'].includes(proxy.type) &&
proxy['client-fingerprint'] &&
proxy['client-fingerprint'] !== ''
)
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
};
@@ -305,7 +254,6 @@ const sshParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -333,7 +281,6 @@ const httpParser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -353,10 +300,8 @@ const socks5Parser = (proxy = {}) => {
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -368,17 +313,6 @@ const shadowTLSParser = (proxy = {}) => {
password: proxy.password,
detour: `${proxy.name}_shadowtls`,
};
if (proxy.uot) ssPart.udp_over_tcp = true;
if (proxy['udp-over-tcp']) {
ssPart.udp_over_tcp = {
enabled: true,
version:
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
? 1
: 2,
};
}
const stPart = {
tag: `${proxy.name}_shadowtls`,
type: 'shadowtls',
@@ -401,7 +335,6 @@ const shadowTLSParser = (proxy = {}) => {
tfoParser(proxy, stPart);
detourParser(proxy, stPart);
smuxParser(proxy.smux, ssPart);
ipVersionParser(proxy, stPart);
return { type: 'ss-with-st', ssPart, stPart };
};
const ssParser = (proxy = {}) => {
@@ -416,22 +349,11 @@ const ssParser = (proxy = {}) => {
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.uot) parsedProxy.udp_over_tcp = true;
if (proxy['udp-over-tcp']) {
parsedProxy.udp_over_tcp = {
enabled: true,
version:
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
? 1
: 2,
};
}
if (proxy['udp-over-tcp']) parsedProxy.udp_over_tcp = true;
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
if (proxy.plugin) {
const optArr = [];
if (proxy.plugin === 'obfs') {
@@ -510,7 +432,6 @@ const ssrParser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -544,12 +465,11 @@ const vmessParser = (proxy = {}) => {
if (proxy.network === 'h2') h2Parser(proxy, parsedProxy);
if (proxy.network === 'http') h1Parser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -564,20 +484,15 @@ const vlessParser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow;
if (proxy.flow != null) parsedProxy.flow = proxy.flow;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
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);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const trojanParser = (proxy = {}) => {
@@ -594,12 +509,11 @@ const trojanParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const hysteriaParser = (proxy = {}) => {
@@ -618,13 +532,12 @@ const hysteriaParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
// eslint-disable-next-line no-control-regex
const reg = new RegExp('^[0-9]+[ \t]*[KMGT]*[Bb]ps$');
// sing-box 跟文档不一致, 但是懒得全转, 只处理最常见的 Mbps
if (reg.test(`${proxy.up}`) && !`${proxy.up}`.endsWith('Mbps')) {
if (reg.test(`${proxy.up}`)) {
parsedProxy.up = `${proxy.up}`;
} else {
parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
}
if (reg.test(`${proxy.down}`) && !`${proxy.down}`.endsWith('Mbps')) {
if (reg.test(`${proxy.down}`)) {
parsedProxy.down = `${proxy.down}`;
} else {
parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
@@ -644,12 +557,10 @@ const hysteriaParser = (proxy = {}) => {
parsedProxy.disable_mtu_discovery = true;
}
}
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
@@ -664,26 +575,16 @@ const hysteria2Parser = (proxy = {}) => {
};
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
throw 'invalid port';
if (proxy['hop-interval'])
parsedProxy.hop_interval = /^\d+$/.test(proxy['hop-interval'])
? `${proxy['hop-interval']}s`
: proxy['hop-interval'];
if (proxy['ports'])
parsedProxy.server_ports = proxy['ports']
.split(/\s*,\s*/)
.map((p) => p.replace(/\s*-\s*/g, ':'));
if (proxy.up) parsedProxy.up_mbps = parseInt(`${proxy.up}`, 10);
if (proxy.down) parsedProxy.down_mbps = parseInt(`${proxy.down}`, 10);
if (proxy.obfs === 'salamander') parsedProxy.obfs.type = 'salamander';
if (proxy['obfs-password'])
parsedProxy.obfs.password = proxy['obfs-password'];
if (!parsedProxy.obfs.type) delete parsedProxy.obfs;
networkParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const tuic5Parser = (proxy = {}) => {
@@ -710,35 +611,10 @@ const tuic5Parser = (proxy = {}) => {
if (proxy['udp-over-stream']) parsedProxy.udp_over_stream = true;
if (proxy['heartbeat-interval'])
parsedProxy.heartbeat = `${proxy['heartbeat-interval']}ms`;
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const anytlsParser = (proxy = {}) => {
const parsedProxy = {
tag: proxy.name,
type: 'anytls',
server: proxy.server,
server_port: parseInt(`${proxy.port}`, 10),
password: proxy.password,
tls: { enabled: true, server_name: proxy.server, insecure: false },
};
if (/^\d+$/.test(proxy['idle-session-check-interval']))
parsedProxy.idle_session_check_interval = `${proxy['idle-session-check-interval']}s`;
if (/^\d+$/.test(proxy['idle-session-timeout']))
parsedProxy.idle_session_timeout = `${proxy['idle-session-timeout']}s`;
if (/^\d+$/.test(proxy['min-idle-session']))
parsedProxy.min_idle_session = parseInt(
`${proxy['min-idle-session']}`,
10,
);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -792,11 +668,9 @@ const wireguardParser = (proxy = {}) => {
parsedProxy.peers.push(peer);
}
}
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -911,12 +785,7 @@ export default function singbox_Producer() {
list.push(hysteriaParser(proxy));
break;
case 'hysteria2':
list.push(
hysteria2Parser(
proxy,
opts['include-unsupported-proxy'],
),
);
list.push(hysteria2Parser(proxy));
break;
case 'tuic':
if (!proxy.token || proxy.token.length === 0) {
@@ -930,9 +799,6 @@ export default function singbox_Producer() {
case 'wireguard':
list.push(wireguardParser(proxy));
break;
case 'anytls':
list.push(anytlsParser(proxy));
break;
default:
throw new Error(
`Platform sing-box does not support proxy type: ${proxy.type}`,

View File

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

View File

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

View File

@@ -14,21 +14,15 @@ const ipVersions = {
export default function Surge_Producer() {
const produce = (proxy, type, opts = {}) => {
proxy.name = proxy.name.replace(/=|,/g, '');
if (proxy.ports) {
proxy.ports = String(proxy.ports);
}
switch (proxy.type) {
case 'ss':
return shadowsocks(proxy, opts['include-unsupported-proxy']);
return shadowsocks(proxy);
case 'trojan':
return trojan(proxy);
case 'vmess':
return vmess(proxy, opts['include-unsupported-proxy']);
return vmess(proxy);
case 'http':
return http(proxy);
case 'direct':
return direct(proxy);
case 'socks5':
return socks5(proxy);
case 'snell':
@@ -87,14 +81,12 @@ function shadowsocks(proxy) {
'chacha20',
'chacha20-ietf',
'none',
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
].includes(proxy.cipher)
) {
throw new Error(`cipher ${proxy.cipher} is not supported`);
}
result.append(`,encrypt-method=${proxy.cipher}`);
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -157,8 +149,6 @@ function shadowsocks(proxy) {
`,shadow-tls-sni=${proxy['shadow-tls-sni']}`,
'shadow-tls-sni',
);
// udp-port
result.appendIfPresent(`,udp-port=${proxy['udp-port']}`, 'udp-port');
} else if (['shadow-tls'].includes(proxy.plugin) && proxy['plugin-opts']) {
const password = proxy['plugin-opts'].password;
const host = proxy['plugin-opts'].host;
@@ -176,11 +166,6 @@ function shadowsocks(proxy) {
}
result.append(`,shadow-tls-version=${version}`);
}
// udp-port
result.appendIfPresent(
`,udp-port=${proxy['udp-port']}`,
'udp-port',
);
}
}
@@ -199,7 +184,7 @@ function shadowsocks(proxy) {
function trojan(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -278,7 +263,7 @@ function trojan(proxy) {
return result.toString();
}
function vmess(proxy, includeUnsupportedProxy) {
function vmess(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
@@ -292,7 +277,7 @@ function vmess(proxy, includeUnsupportedProxy) {
);
// transport
handleTransport(result, proxy, includeUnsupportedProxy);
handleTransport(result, proxy);
// AEAD
if (isPresent(proxy, 'aead')) {
@@ -370,9 +355,9 @@ function vmess(proxy, includeUnsupportedProxy) {
function ssh(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,${proxy.username}`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,${proxy.password}`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
@@ -433,14 +418,11 @@ function ssh(proxy) {
return result.toString();
}
function http(proxy) {
if (proxy.headers && Object.keys(proxy.headers).length > 0) {
throw new Error(`headers is unsupported`);
}
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
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');
@@ -512,61 +494,13 @@ function http(proxy) {
return result.toString();
}
function direct(proxy) {
const result = new Result(proxy);
const type = 'direct';
result.append(`${proxy.name}=${type}`);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
// tfo
result.appendIfPresent(`,tfo=${proxy.tfo}`, 'tfo');
// udp
result.appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
result.appendIfPresent(
`,test-timeout=${proxy['test-timeout']}`,
'test-timeout',
);
result.appendIfPresent(`,test-udp=${proxy['test-udp']}`, 'test-udp');
result.appendIfPresent(`,hybrid=${proxy['hybrid']}`, 'hybrid');
result.appendIfPresent(`,tos=${proxy['tos']}`, 'tos');
result.appendIfPresent(
`,allow-other-interface=${proxy['allow-other-interface']}`,
'allow-other-interface',
);
result.appendIfPresent(
`,interface=${proxy['interface-name']}`,
'interface-name',
);
// block-quic
result.appendIfPresent(`,block-quic=${proxy['block-quic']}`, 'block-quic');
// underlying-proxy
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
return result.toString();
}
function socks5(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
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');
@@ -732,7 +666,7 @@ function tuic(proxy) {
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,uuid=${proxy.uuid}`, 'uuid');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
result.appendIfPresent(`,password=${proxy.password}`, 'password');
result.appendIfPresent(`,token=${proxy.token}`, 'token');
result.appendIfPresent(
@@ -740,15 +674,6 @@ function tuic(proxy) {
'alpn',
);
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -1007,16 +932,7 @@ function hysteria2(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=hysteria2,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
result.appendIfPresent(`,password=${proxy.password}`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -1096,7 +1012,7 @@ function hysteria2(proxy) {
return result.toString();
}
function handleTransport(result, proxy, includeUnsupportedProxy) {
function handleTransport(result, proxy) {
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {
result.append(`,ws=true`);
@@ -1122,13 +1038,7 @@ function handleTransport(result, proxy, includeUnsupportedProxy) {
}
}
} else {
if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
$.info(
`Include Unsupported Proxy: nework ${proxy.network} -> tcp`,
);
} else {
throw new Error(`network ${proxy.network} is unsupported`);
}
throw new Error(`network ${proxy.network} is unsupported`);
}
}
}

View File

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

View File

@@ -10,43 +10,23 @@ export default function URI_Producer() {
delete proxy.collectionName;
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
for (const key in proxy) {
if (proxy[key] == null) {
if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
}
if (
['trojan', 'tuic', 'hysteria', 'hysteria2', 'juicity'].includes(
proxy.type,
)
) {
if (['trojan', 'tuic', 'hysteria', 'hysteria2'].includes(proxy.type)) {
delete proxy.tls;
}
if (
!['vmess'].includes(proxy.type) &&
proxy.server &&
isIPv6(proxy.server)
) {
if (proxy.server && isIPv6(proxy.server)) {
proxy.server = `[${proxy.server}]`;
}
switch (proxy.type) {
case 'socks5':
result = `socks://${encodeURIComponent(
Base64.encode(
`${proxy.username ?? ''}:${proxy.password ?? ''}`,
),
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
break;
case 'ss':
const userinfo = `${proxy.cipher}:${proxy.password}`;
result = `ss://${
proxy.cipher?.startsWith('2022-blake3-')
? `${encodeURIComponent(
proxy.cipher,
)}:${encodeURIComponent(proxy.password)}`
: Base64.encode(userinfo)
}@${proxy.server}:${proxy.port}${proxy.plugin ? '/' : ''}`;
result = `ss://${Base64.encode(userinfo)}@${proxy.server}:${
proxy.port
}${proxy.plugin ? '/' : ''}`;
if (proxy.plugin) {
result += '?plugin=';
const opts = proxy['plugin-opts'];
@@ -65,11 +45,6 @@ export default function URI_Producer() {
}${opts.tls ? ';tls' : ''}`,
);
break;
case 'shadow-tls':
result += encodeURIComponent(
`shadow-tls;host=${opts.host};password=${opts.password};version=${opts.version}`,
);
break;
default:
throw new Error(
`Unsupported plugin option: ${proxy.plugin}`,
@@ -119,17 +94,12 @@ export default function URI_Producer() {
v: '2',
ps: proxy.name,
add: proxy.server,
port: `${proxy.port}`,
port: proxy.port,
id: proxy.uuid,
aid: `${proxy.alterId || 0}`,
scy: proxy.cipher,
net,
type,
aid: 0,
net,
tls: proxy.tls ? 'tls' : '',
alpn: Array.isArray(proxy.alpn)
? proxy.alpn.join(',')
: proxy.alpn,
fp: proxy['client-fingerprint'],
};
if (proxy.tls && proxy.sni) {
result.sni = proxy.sni;
@@ -140,7 +110,16 @@ export default function URI_Producer() {
proxy[`${proxy.network}-opts`]?.path;
let vmessTransportHost =
proxy[`${proxy.network}-opts`]?.headers?.Host;
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
if (['grpc'].includes(proxy.network)) {
result.path =
proxy[`${proxy.network}-opts`]?.[
@@ -150,33 +129,6 @@ export default function URI_Producer() {
result.type =
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun';
result.host =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
} else if (['kcp', 'quic'].includes(proxy.network)) {
// https://github.com/XTLS/Xray-core/issues/91
result.type =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-type`
] || 'none';
result.host =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-host`
];
result.path =
proxy[`${proxy.network}-opts`]?.[
`_${proxy.network}-path`
];
} else {
if (vmessTransportPath) {
result.path = Array.isArray(vmessTransportPath)
? vmessTransportPath[0]
: vmessTransportPath;
}
if (vmessTransportHost) {
result.host = Array.isArray(vmessTransportHost)
? vmessTransportHost[0]
: vmessTransportHost;
}
}
}
result = 'vmess://' + Base64.encode(JSON.stringify(result));
@@ -186,7 +138,6 @@ export default function URI_Producer() {
const isReality = proxy['reality-opts'];
let sid = '';
let pbk = '';
let spx = '';
if (isReality) {
security = 'reality';
const publicKey = proxy['reality-opts']?.['public-key'];
@@ -197,10 +148,6 @@ export default function URI_Producer() {
if (shortId) {
sid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
spx = `&spx=${encodeURIComponent(spiderX)}`;
}
} else if (proxy.tls) {
security = 'tls';
}
@@ -230,14 +177,6 @@ export default function URI_Producer() {
if (proxy.flow) {
flow = `&flow=${encodeURIComponent(proxy.flow)}`;
}
let extra = '';
if (proxy._extra) {
extra = `&extra=${encodeURIComponent(proxy._extra)}`;
}
let mode = '';
if (proxy._mode) {
mode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
let vlessType = proxy.network;
if (
proxy.network === 'ws' &&
@@ -252,13 +191,6 @@ export default function URI_Producer() {
vlessTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] || 'gun',
)}`;
const authority =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
if (authority) {
vlessTransport += `&authority=${encodeURIComponent(
authority,
)}`;
}
}
let vlessTransportServiceName =
@@ -287,24 +219,12 @@ export default function URI_Producer() {
vlessTransportServiceName,
)}`;
}
if (proxy.network === 'kcp') {
if (proxy.seed) {
vlessTransport += `&seed=${encodeURIComponent(
proxy.seed,
)}`;
}
if (proxy.headerType) {
vlessTransport += `&headerType=${encodeURIComponent(
proxy.headerType,
)}`;
}
}
result = `vless://${proxy.uuid}@${proxy.server}:${
proxy.port
}?security=${encodeURIComponent(
security,
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${spx}${pbk}${mode}${extra}#${encodeURIComponent(
)}${vlessTransport}${alpn}${allowInsecure}${sni}${fp}${flow}${sid}${pbk}#${encodeURIComponent(
proxy.name,
)}`;
break;
@@ -324,18 +244,11 @@ export default function URI_Producer() {
proxy[`${proxy.network}-opts`]?.[
`${proxy.network}-service-name`
];
let trojanTransportAuthority =
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
if (trojanTransportServiceName) {
trojanTransport += `&serviceName=${encodeURIComponent(
trojanTransportServiceName,
)}`;
}
if (trojanTransportAuthority) {
trojanTransport += `&authority=${encodeURIComponent(
trojanTransportAuthority,
)}`;
}
trojanTransport += `&mode=${encodeURIComponent(
proxy[`${proxy.network}-opts`]?.['_grpc-type'] ||
'gun',
@@ -360,68 +273,14 @@ export default function URI_Producer() {
)}`;
}
}
let trojanFp = '';
if (proxy['client-fingerprint']) {
trojanFp = `&fp=${encodeURIComponent(
proxy['client-fingerprint'],
)}`;
}
let trojanAlpn = '';
if (proxy.alpn) {
trojanAlpn = `&alpn=${encodeURIComponent(
Array.isArray(proxy.alpn)
? proxy.alpn
: proxy.alpn.join(','),
)}`;
}
const trojanIsReality = proxy['reality-opts'];
let trojanSid = '';
let trojanPbk = '';
let trojanSpx = '';
let trojanSecurity = '';
let trojanMode = '';
let trojanExtra = '';
if (trojanIsReality) {
trojanSecurity = `&security=reality`;
const publicKey = proxy['reality-opts']?.['public-key'];
if (publicKey) {
trojanPbk = `&pbk=${encodeURIComponent(publicKey)}`;
}
const shortId = proxy['reality-opts']?.['short-id'];
if (shortId) {
trojanSid = `&sid=${encodeURIComponent(shortId)}`;
}
const spiderX = proxy['reality-opts']?.['_spider-x'];
if (spiderX) {
trojanSpx = `&spx=${encodeURIComponent(spiderX)}`;
}
if (proxy._extra) {
trojanExtra = `&extra=${encodeURIComponent(
proxy._extra,
)}`;
}
if (proxy._mode) {
trojanMode = `&mode=${encodeURIComponent(proxy._mode)}`;
}
}
result = `trojan://${proxy.password}@${proxy.server}:${
proxy.port
}?sni=${encodeURIComponent(proxy.sni || proxy.server)}${
proxy['skip-cert-verify'] ? '&allowInsecure=1' : ''
}${trojanTransport}${trojanAlpn}${trojanFp}${trojanSecurity}${trojanSid}${trojanPbk}${trojanSpx}${trojanMode}${trojanExtra}#${encodeURIComponent(
proxy.name,
)}`;
}${trojanTransport}#${encodeURIComponent(proxy.name)}`;
break;
case 'hysteria2':
let hysteria2params = [];
if (proxy['hop-interval']) {
hysteria2params.push(
`hop-interval=${proxy['hop-interval']}`,
);
}
if (proxy['keepalive']) {
hysteria2params.push(`keepalive=${proxy['keepalive']}`);
}
if (proxy['skip-cert-verify']) {
hysteria2params.push(`insecure=1`);
}
@@ -501,7 +360,7 @@ export default function URI_Producer() {
hysteriaParams.push(`obfsParam=${proxy[key]}`);
} else if (['sni'].includes(key)) {
hysteriaParams.push(`peer=${proxy[key]}`);
} else if (proxy[key] && !/^_/i.test(key)) {
} else if (proxy[key]) {
hysteriaParams.push(
`${i}=${encodeURIComponent(proxy[key])}`,
);
@@ -528,7 +387,6 @@ export default function URI_Producer() {
'password',
'server',
'port',
'tls',
].includes(key)
) {
const i = key.replace(/-/, '_');
@@ -557,19 +415,10 @@ export default function URI_Producer() {
['disable-sni', 'reduce-rtt'].includes(key) &&
proxy[key]
) {
tuicParams.push(`${i.replace(/-/g, '_')}=1`);
} else if (
['congestion-controller'].includes(key)
) {
tuicParams.push(`${i}=1`);
} else if (proxy[key]) {
tuicParams.push(
`congestion_control=${proxy[key]}`,
);
} else if (proxy[key] && !/^_/i.test(key)) {
tuicParams.push(
`${i.replace(
/-/g,
'_',
)}=${encodeURIComponent(proxy[key])}`,
`${i}=${encodeURIComponent(proxy[key])}`,
);
}
}
@@ -584,54 +433,6 @@ export default function URI_Producer() {
)}`;
}
break;
case 'anytls':
let anytlsParams = [];
Object.keys(proxy).forEach((key) => {
if (
![
'name',
'type',
'password',
'server',
'port',
'tls',
].includes(key)
) {
const i = key.replace(/-/, '_');
if (['alpn'].includes(key)) {
if (proxy[key]) {
anytlsParams.push(
`${i}=${encodeURIComponent(
Array.isArray(proxy[key])
? proxy[key][0]
: proxy[key],
)}`,
);
}
} else if (['skip-cert-verify'].includes(key)) {
if (proxy[key]) {
anytlsParams.push(`insecure=1`);
}
} else if (['udp'].includes(key)) {
if (proxy[key]) {
anytlsParams.push(`udp=1`);
}
} else if (proxy[key] && !/^_/i.test(key)) {
anytlsParams.push(
`${i.replace(/-/g, '_')}=${encodeURIComponent(
proxy[key],
)}`,
);
}
}
});
result = `anytls://${encodeURIComponent(proxy.password)}@${
proxy.server
}:${proxy.port}/?${anytlsParams.join('&')}#${encodeURIComponent(
proxy.name,
)}`;
break;
case 'wireguard':
let wireguardParams = [];
@@ -653,7 +454,7 @@ export default function URI_Producer() {
if (proxy[key]) {
wireguardParams.push(`${key}=1`);
}
} else if (proxy[key] && !/^_/i.test(key)) {
} else if (proxy[key]) {
wireguardParams.push(
`${key}=${encodeURIComponent(proxy[key])}`,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -112,6 +112,7 @@ function replaceArtifact(req, res) {
async function getArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
@@ -162,6 +163,7 @@ function createArtifact(req, res) {
function updateArtifact(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
let oldName = req.params.name;
oldName = decodeURIComponent(oldName);
const artifact = findByName(allArtifacts, oldName);
if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`);
@@ -195,6 +197,7 @@ function updateArtifact(req, res) {
async function deleteArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除远程配置:${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
try {

View File

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

View File

@@ -1,11 +1,8 @@
import {
getPlatformFromHeaders,
shouldIncludeUnsupportedProxy,
} from '@/utils/user-agent';
import { getPlatformFromHeaders } from '@/utils/user-agent';
import { ProxyUtils } from '@/core/proxy-utils';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import { getFlowHeaders } from '@/utils/flow';
import $ from '@/core/app';
import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -16,44 +13,8 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env';
export default function register($app) {
$app.get('/share/col/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/share/sub/:name', downloadSubscription);
$app.get('/download/collection/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadCollection(req, res);
});
$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name/:target', async (req, res) => {
const { target } = req.params;
if (target) {
req.query.target = target;
$.info(`使用路由指定目标: ${target}`);
}
await downloadSubscription(req, res);
});
$app.get('/download/:name', downloadSubscription);
$app.get(
'/download/collection/:name/api/v1/server/details',
async (req, res) => {
@@ -88,15 +49,13 @@ export default function register($app) {
async function downloadSubscription(req, res) {
let { name, nezhaIndex } = req.params;
const useMihomoExternal = req.query.target === 'SurgeMac';
name = decodeURIComponent(name);
nezhaIndex = decodeURIComponent(nezhaIndex);
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
$.info(`正在下载订阅:${name}`);
let {
url,
ua,
@@ -106,98 +65,40 @@ async function downloadSubscription(req, res) {
produceType,
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) {
url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`);
if (!/^https?:\/\//.test(url)) {
content = url;
$.info(`URL 不是链接,视为本地订阅`);
}
}
if (content) {
$.info(`指定本地订阅: ${content}`);
}
if (proxy) {
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
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) {
$.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (sub) {
try {
const passThroughUA = sub.passThroughUA;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${reqUA}`,
);
ua = reqUA;
}
let output = await produceArtifact({
type: 'subscription',
name,
@@ -210,23 +111,18 @@ async function downloadSubscription(req, res) {
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
useMihomoExternal,
},
$options,
proxy,
noCache,
});
let 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] || '';
url = `${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
@@ -249,20 +145,16 @@ async function downloadSubscription(req, res) {
}
}
}
if (!$arguments.noFlow && /^https?/.test(url)) {
if (!$arguments.noFlow) {
// forward flow headers
flowInfo = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy,
$arguments.flowUrl,
sub.proxy,
);
if (flowInfo) {
res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
@@ -274,32 +166,7 @@ async function downloadSubscription(req, res) {
}
}
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
res.set(
'subscription-userinfo',
normalizeFlowHeader(
[subUserInfo, flowInfo].filter((i) => i).join(';'),
),
);
res.set('subscription-userinfo', sub.subUserinfo);
}
if (platform === 'JSON') {
@@ -338,7 +205,7 @@ async function downloadSubscription(req, res) {
);
}
} else {
$.error(`🌍 Sub-Store 下载订阅失败\n❌ 未找到订阅:${name}`);
$.notify(`🌍 Sub-Store 下载订阅失败`, `❌ 未找到订阅:${name}`);
failed(
res,
new ResourceNotFoundError(
@@ -352,87 +219,36 @@ async function downloadSubscription(req, res) {
async function downloadCollection(req, res) {
let { name, nezhaIndex } = req.params;
const useMihomoExternal = req.query.target === 'SurgeMac';
name = decodeURIComponent(name);
nezhaIndex = decodeURIComponent(nezhaIndex);
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);
const reqUA = req.headers['user-agent'] || req.headers['User-Agent'];
$.info(
`正在下载组合订阅:${name}\n请求 User-Agent: ${reqUA}\n请求 target: ${req.query.target}\n实际输出: ${platform}`,
);
$.info(`正在下载组合订阅:${name}`);
let {
ignoreFailedRemoteSub,
produceType,
includeUnsupportedProxy,
resultFormat,
proxy,
noCache,
} = req.query;
let $options = {
_req: {
method: req.method,
url: req.url,
path: req.path,
query: req.query,
params: req.params,
headers: req.headers,
body: req.body,
},
};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (proxy) {
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
}
if (produceType) {
produceType = decodeURIComponent(produceType);
$.info(`指定生产类型: ${produceType}`);
}
if (includeUnsupportedProxy) {
$.info(
`包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (
!includeUnsupportedProxy &&
shouldIncludeUnsupportedProxy(platform, reqUA)
) {
includeUnsupportedProxy = true;
$.info(
`当前客户端可包含官方/商店版/未续费订阅不支持的协议: ${includeUnsupportedProxy}`,
);
}
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
if (noCache) {
$.info(`指定不使用缓存: ${noCache}`);
includeUnsupportedProxy = decodeURIComponent(includeUnsupportedProxy);
$.info(`包含不支持的节点: ${includeUnsupportedProxy}`);
}
if (collection) {
@@ -445,14 +261,9 @@ async function downloadCollection(req, res) {
produceType,
produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy,
useMihomoExternal,
},
$options,
proxy,
noCache,
ua: reqUA,
});
let subUserInfoOfSub;
// forward flow header from the first subscription in this collection
const allSubs = $.read(SUBS_KEY);
const subnames = collection.subscriptions;
@@ -463,11 +274,10 @@ async function downloadCollection(req, res) {
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try {
let url =
`${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let url = `${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
@@ -490,14 +300,16 @@ async function downloadCollection(req, res) {
}
}
}
if (!$arguments.noFlow && /^https?:/.test(url)) {
subUserInfoOfSub = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy || sub.proxy || collection.proxy,
$arguments.flowUrl,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
} catch (err) {
$.error(
@@ -508,63 +320,10 @@ async function downloadCollection(req, res) {
}
}
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`组合订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
subUserInfoOfSub = [subUserInfo, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
res.set('subscription-userinfo', sub.subUserinfo);
}
}
$.info(`组合订阅 ${name} 透传的的流量信息: ${subUserInfoOfSub}`);
let subUserInfoOfCol;
if (/^https?:\/\//.test(collection.subUserinfo)) {
try {
subUserInfoOfCol = await getFlowHeaders(
undefined,
undefined,
undefined,
proxy || collection.proxy,
collection.subUserinfo,
);
} catch (e) {
$.error(
`组合订阅 ${name} 使用自定义流量链接 ${
collection.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfoOfCol = collection.subUserinfo;
}
const subUserInfo = [subUserInfoOfCol, subUserInfoOfSub]
.filter((i) => i)
.join('; ');
if (subUserInfo) {
res.set(
'subscription-userinfo',
normalizeFlowHeader(subUserInfo),
);
}
if (platform === 'JSON') {
if (resultFormat === 'nezha') {
output = nezhaTransform(output);
@@ -600,7 +359,7 @@ async function downloadCollection(req, res) {
);
}
} else {
$.error(
$.notify(
`🌍 Sub-Store 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);

View File

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

View File

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

View File

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

View File

@@ -43,6 +43,7 @@ function createModule(req, res) {
function getModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
@@ -63,6 +64,7 @@ function getModule(req, res) {
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);
@@ -90,6 +92,7 @@ function updateModule(req, res) {
function deleteModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除模块:${name}`);
let allModules = $.read(MODULES_KEY);
deleteByName(allModules, name);

View File

@@ -15,60 +15,46 @@ export default function register($app) {
async function previewFile(req, res) {
try {
const file = req.body;
let content = '';
if (file.type !== 'mihomoProfile') {
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,
undefined,
file.proxy,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
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 (Object.keys(errors).length > 0) {
if (!file.ignoreFailedRemoteFile) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (file.ignoreFailedRemoteFile === 'enabled') {
$.notify(
`🌍 Sub-Store 预览文件失败`,
`${file.name}`,
`远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (file.mergeSources === 'localFirst') {
content.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
content.push(file.content);
}
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
@@ -81,7 +67,7 @@ async function previewFile(req, res) {
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{ $files: files, $content: filesContent, $file: file },
{ $files: files, $content: filesContent },
file.process,
)
: { $content: filesContent, $files: files };
@@ -128,10 +114,6 @@ async function compareSub(req, res) {
sub.ua,
undefined,
sub.proxy,
undefined,
undefined,
undefined,
true,
);
} catch (err) {
errors[url] = err;
@@ -143,22 +125,12 @@ async function compareSub(req, res) {
}),
);
if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
} else if (sub.ignoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 预览订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (!sub.ignoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
content.unshift(sub.content);
@@ -174,8 +146,7 @@ async function compareSub(req, res) {
// add id
original.forEach((proxy, i) => {
proxy.id = i;
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
proxy.subName = sub.name;
});
// apply processors
@@ -205,20 +176,7 @@ async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = [...collection.subscriptions];
let subscriptionTags = collection.subscriptionTags;
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
allSubs.forEach((sub) => {
if (
Array.isArray(sub.tag) &&
sub.tag.length > 0 &&
!subnames.includes(sub.name) &&
sub.tag.some((tag) => subscriptionTags.includes(tag))
) {
subnames.push(sub.name);
}
});
}
const subnames = collection.subscriptions;
const results = {};
const errors = {};
await Promise.all(
@@ -247,10 +205,6 @@ async function compareCollection(req, res) {
sub.ua,
undefined,
sub.proxy,
undefined,
undefined,
undefined,
true,
);
} catch (err) {
errors[url] = err;
@@ -261,25 +215,15 @@ async function compareCollection(req, res) {
}
}),
);
if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (
sub.ignoreFailedRemoteSub === 'enabled'
) {
$.notify(
`🌍 Sub-Store 预览订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
@@ -293,10 +237,8 @@ async function compareCollection(req, res) {
.flat();
currentProxies.forEach((proxy) => {
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors
@@ -311,28 +253,20 @@ async function compareCollection(req, res) {
errors[name] = err;
$.error(
`❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name} 时出现错误:${err}`,
`❌ 处理组合订阅 ${collection.name} 中的子订阅: ${sub.name}时出现错误:${err}`,
);
}
}),
);
if (Object.keys(errors).length > 0) {
if (!collection.ignoreFailedRemoteSub) {
throw new Error(
`组合订阅 ${collection.name} 的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (collection.ignoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 预览组合订阅失败`,
`${collection.name}`,
`子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
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(
@@ -342,8 +276,7 @@ async function compareCollection(req, res) {
original.forEach((proxy, i) => {
proxy.id = i;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
proxy.collectionName = collection.name;
});
const processed = await ProxyUtils.process(

View File

@@ -12,9 +12,7 @@ export function failed(resp, error, statusCode) {
code: error.code,
type: error.type,
message: error.message,
details: resp.req?.route?.path?.startsWith('/share/')
? '详情请查看日志'
: error.details,
details: error.details,
},
});
}

View File

@@ -61,7 +61,7 @@ async function updateSettings(req, res) {
export async function updateAvatar() {
const settings = $.read(SETTINGS_KEY);
const { githubUser: username, syncPlatform, githubProxy } = settings;
const { githubUser: username, syncPlatform } = settings;
if (username) {
if (syncPlatform === 'gitlab') {
try {
@@ -92,9 +92,7 @@ export async function updateAvatar() {
try {
const data = await $.http
.get({
url: `${
githubProxy ? `${githubProxy}/` : ''
}https://api.github.com/users/${encodeURIComponent(
url: `https://api.github.com/users/${encodeURIComponent(
username,
)}`,
headers: {
@@ -136,15 +134,11 @@ export async function updateArtifactStore() {
settings.artifactStore = url;
settings.artifactStoreStatus = 'VALID';
} else {
$.error(`找不到 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY})`);
$.error(`找不到 Sub-Store Gist`);
settings.artifactStoreStatus = 'NOT FOUND';
}
} catch (err) {
$.error(
`查找 Sub-Store Gist (${ARTIFACT_REPOSITORY_KEY}) 时发生错误: ${
err.message ?? err
}`,
);
$.error(`查找 Sub-Store Gist 时发生错误: ${err.message ?? err}`);
settings.artifactStoreStatus = 'ERROR';
}
$.write(settings, SETTINGS_KEY);

View File

@@ -5,12 +5,7 @@ import {
RequestInvalidError,
} from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database';
import {
SUBS_KEY,
COLLECTIONS_KEY,
ARTIFACTS_KEY,
FILES_KEY,
} from '@/constants';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import {
getFlowHeaders,
parseFlowHeaders,
@@ -18,7 +13,6 @@ import {
} from '@/utils/flow';
import { success, failed } from './response';
import $ from '@/core/app';
import { formatDateTime } from '@/utils';
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
@@ -39,10 +33,7 @@ export default function register($app) {
// subscriptions API
async function getFlowInfo(req, res) {
let { name } = req.params;
let { url } = req.query;
if (url) {
$.info(`指定远程订阅 URL: ${url}`);
}
name = decodeURIComponent(name);
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
if (!sub) {
@@ -61,45 +52,9 @@ async function getFlowInfo(req, res) {
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
if (sub.subUserinfo) {
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
try {
success(res, {
...parseFlowHeaders(subUserInfo),
});
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
);
}
success(res, {
...parseFlowHeaders(sub.subUserinfo),
});
} else {
failed(
res,
@@ -113,11 +68,10 @@ async function getFlowInfo(req, res) {
return;
}
try {
url =
`${url || sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0] || '';
let url = `${sub.url}`
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
@@ -138,7 +92,7 @@ async function getFlowInfo(req, res) {
}
}
}
if ($arguments.noFlow || !/^https?/.test(url)) {
if ($arguments.noFlow) {
failed(
res,
new RequestInvalidError(
@@ -149,73 +103,41 @@ async function getFlowInfo(req, res) {
);
return;
}
const flowHeaders = await getFlowHeaders(
$arguments?.insecure ? `${url}#insecure` : url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
$arguments.flowUrl,
);
if (!flowHeaders && !sub.subUserinfo) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
return;
}
try {
const remainingDays = getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
if (sub.subUserinfo) {
success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
let subUserInfo;
if (/^https?:\/\//.test(sub.subUserinfo)) {
try {
subUserInfo = await getFlowHeaders(
undefined,
undefined,
undefined,
sub.proxy,
sub.subUserinfo,
);
} catch (e) {
$.error(
`订阅 ${name} 使用自定义流量链接 ${
sub.subUserinfo
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
);
}
} else {
subUserInfo = sub.subUserinfo;
}
const result = {
...parseFlowHeaders(
[subUserInfo, flowHeaders].filter((i) => i).join('; '),
),
};
if (remainingDays != null) {
result.remainingDays = remainingDays;
}
success(res, result);
} catch (e) {
$.error(
`Failed to parse flow info for local subscription ${name}: ${
e.message ?? e
}`,
);
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Failed to parse flow info`,
),
} 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(
@@ -230,7 +152,6 @@ async function getFlowInfo(req, res) {
function createSubscription(req, res) {
const sub = req.body;
delete sub.subscriptions;
$.info(`正在创建订阅: ${sub.name}`);
if (/\//.test(sub.name)) {
failed(
@@ -261,19 +182,15 @@ function createSubscription(req, res) {
function getSubscription(req, res) {
let { name } = req.params;
let { raw } = req.query;
name = decodeURIComponent(name);
const allSubs = $.read(SUBS_KEY);
const sub = findByName(allSubs, name);
delete sub.subscriptions;
if (sub) {
if (raw) {
res.set('content-type', 'application/json')
.set(
'content-disposition',
`attachment; filename="${encodeURIComponent(
`sub-store_subscription_${name}_${formatDateTime(
new Date(),
)}.json`,
)}"`,
`attachment; filename="${encodeURIComponent(name)}.json"`,
)
.send(JSON.stringify(sub));
} else {
@@ -293,8 +210,8 @@ function getSubscription(req, res) {
function updateSubscription(req, res) {
let { name } = req.params;
name = decodeURIComponent(name); // the original name
let sub = req.body;
delete sub.subscriptions;
const allSubs = $.read(SUBS_KEY);
const oldSub = findByName(allSubs, name);
if (oldSub) {
@@ -324,20 +241,9 @@ function updateSubscription(req, res) {
artifact.source = sub.name;
}
}
// update all files referring this subscription
const allFiles = $.read(FILES_KEY) || [];
for (const file of allFiles) {
if (
file.sourceType === 'subscription' &&
file.sourceName == name
) {
file.sourceName = sub.name;
}
}
$.write(allCols, COLLECTIONS_KEY);
$.write(allArtifacts, ARTIFACTS_KEY);
$.write(allFiles, FILES_KEY);
}
updateByName(allSubs, name, newSub);
$.write(allSubs, SUBS_KEY);
@@ -356,6 +262,7 @@ function updateSubscription(req, res) {
function deleteSubscription(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`删除订阅:${name}...`);
// delete from subscriptions
let allSubs = $.read(SUBS_KEY);

View File

@@ -36,15 +36,10 @@ async function produceArtifact({
produceType,
produceOpts = {},
subscription,
awaitCustomCache,
$options,
proxy,
noCache,
all,
}) {
platform = platform || 'JSON';
if (['subscription', 'sub'].includes(type)) {
if (type === 'subscription') {
let sub;
if (name) {
const allSubs = $.read(SUBS_KEY);
@@ -71,11 +66,7 @@ async function produceArtifact({
url,
ua || sub.ua,
undefined,
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache || sub.noCache,
true,
sub.proxy,
);
} catch (err) {
errors[url] = err;
@@ -90,23 +81,12 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (Object.keys(errors).length > 0) {
if (!subIgnoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
} else if (subIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (mergeSources === 'localFirst') {
raw.unshift(content);
@@ -131,11 +111,7 @@ async function produceArtifact({
url,
ua || sub.ua,
undefined,
proxy || sub.proxy,
undefined,
awaitCustomCache,
noCache || sub.noCache,
true,
sub.proxy,
);
} catch (err) {
errors[url] = err;
@@ -150,23 +126,12 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
subIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (Object.keys(errors).length > 0) {
if (!subIgnoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
} else if (subIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (!subIgnoreFailedRemoteSub && Object.keys(errors).length > 0) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
@@ -174,17 +139,13 @@ async function produceArtifact({
raw.push(sub.content);
}
}
if (produceType === 'raw') {
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
}
// parse proxies
let proxies = (Array.isArray(raw) ? raw : [raw])
.map((i) => ProxyUtils.parse(i))
.flat();
proxies.forEach((proxy) => {
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
proxy.subName = sub.name;
});
// apply processors
proxies = await ProxyUtils.process(
@@ -192,7 +153,6 @@ async function produceArtifact({
sub.process || [],
platform,
{ [sub.name]: sub },
$options,
);
if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -216,25 +176,12 @@ async function produceArtifact({
}
// produce
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (['collection', 'col'].includes(type)) {
} 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];
let subscriptionTags = collection.subscriptionTags;
if (Array.isArray(subscriptionTags) && subscriptionTags.length > 0) {
allSubs.forEach((sub) => {
if (
Array.isArray(sub.tag) &&
sub.tag.length > 0 &&
!subnames.includes(sub.name) &&
sub.tag.some((tag) => subscriptionTags.includes(tag))
) {
subnames.push(sub.name);
}
});
}
const subnames = collection.subscriptions;
const results = {};
const errors = {};
let processed = 0;
@@ -242,14 +189,6 @@ async function produceArtifact({
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
const passThroughUA = sub.passThroughUA;
let reqUA = sub.ua;
if (passThroughUA) {
$.info(
`订阅开启了透传 User-Agent, 使用请求的 User-Agent: ${ua}`,
);
reqUA = ua;
}
try {
$.info(`正在处理子订阅:${sub.name}...`);
let raw;
@@ -271,15 +210,9 @@ async function produceArtifact({
try {
return await download(
url,
reqUA,
sub.ua,
undefined,
proxy ||
sub.proxy ||
collection.proxy,
undefined,
undefined,
noCache || sub.noCache,
true,
sub.proxy,
);
} catch (err) {
errors[url] = err;
@@ -290,25 +223,15 @@ async function produceArtifact({
}
}),
);
if (Object.keys(errors).length > 0) {
if (!sub.ignoreFailedRemoteSub) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (
sub.ignoreFailedRemoteSub === 'enabled'
) {
$.notify(
`🌍 Sub-Store 处理订阅失败`,
`${sub.name}`,
`远程订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (
!sub.ignoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`订阅 ${sub.name} 的远程订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
}
if (sub.mergeSources === 'localFirst') {
raw.unshift(sub.content);
@@ -322,10 +245,8 @@ async function produceArtifact({
.flat();
currentProxies.forEach((proxy) => {
proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
proxy.subName = sub.name;
proxy.collectionName = collection.name;
});
// apply processors
@@ -333,11 +254,7 @@ async function produceArtifact({
currentProxies,
sub.process || [],
platform,
{
[sub.name]: sub,
_collection: collection,
$options,
},
{ [sub.name]: sub, _collection: collection },
);
results[name] = currentProxies;
processed++;
@@ -363,23 +280,15 @@ async function produceArtifact({
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
collectionIgnoreFailedRemoteSub = ignoreFailedRemoteSub;
}
if (Object.keys(errors).length > 0) {
if (!collectionIgnoreFailedRemoteSub) {
throw new Error(
`组合订阅 ${collection.name} 的子订阅 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (collectionIgnoreFailedRemoteSub === 'enabled') {
$.notify(
`🌍 Sub-Store 处理组合订阅失败`,
`${collection.name}`,
`子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
if (
!collectionIgnoreFailedRemoteSub &&
Object.keys(errors).length > 0
) {
throw new Error(
`组合订阅 ${name} 中的子订阅 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
// merge proxies with the original order
@@ -389,8 +298,7 @@ async function produceArtifact({
);
proxies.forEach((proxy) => {
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
proxy.collectionName = collection.name;
});
// apply own processors
@@ -399,7 +307,6 @@ async function produceArtifact({
collection.process || [],
platform,
{ _collection: collection },
$options,
);
if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -454,128 +361,90 @@ async function produceArtifact({
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (!file) throw new Error(`找不到文件 ${name}`);
let raw = '';
if (file.type !== 'mihomoProfile') {
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 (
content &&
!['localFirst', 'remoteFirst'].includes(mergeSources)
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
raw = content;
} else if (url) {
const errors = {};
raw = await Promise.all(
url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)
.map(async (url) => {
try {
return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
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,
undefined,
file.proxy || proxy,
undefined,
undefined,
noCache,
);
} catch (err) {
errors[url] = err;
$.error(
`文件 ${file.name} 的远程文件 ${url} 发生错误: ${err}`,
);
return '';
}
}),
);
let fileIgnoreFailedRemoteFile = file.ignoreFailedRemoteFile;
if (
ignoreFailedRemoteFile != null &&
ignoreFailedRemoteFile !== ''
) {
fileIgnoreFailedRemoteFile = ignoreFailedRemoteFile;
}
if (Object.keys(errors).length > 0) {
if (!fileIgnoreFailedRemoteFile) {
throw new Error(
`文件 ${file.name} 的远程文件 ${Object.keys(
errors,
).join(', ')} 发生错误, 请查看日志`,
);
} else if (fileIgnoreFailedRemoteFile === 'enabled') {
$.notify(
`🌍 Sub-Store 处理文件失败`,
`${file.name}`,
`远程文件 ${Object.keys(errors).join(
', ',
)} 发生错误, 请查看日志`,
);
}
}
if (file.mergeSources === 'localFirst') {
raw.unshift(file.content);
} else if (file.mergeSources === 'remoteFirst') {
raw.push(file.content);
}
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);
}
}
if (produceType === 'raw') {
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
}
const files = (Array.isArray(raw) ? raw : [raw]).flat();
let filesContent = files
@@ -586,17 +455,12 @@ async function produceArtifact({
const processed =
Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process(
{
$files: files,
$content: filesContent,
$options,
$file: file,
},
{ $files: files, $content: filesContent },
file.process,
)
: { $content: filesContent, $files: files, $options };
: { $content: filesContent, $files: files };
return (all ? processed : processed?.$content) ?? '';
return processed?.$content ?? '';
}
}
@@ -606,15 +470,12 @@ async function syncArtifacts() {
const files = {};
try {
const valid = [];
const invalid = [];
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const subNames = [];
let enabledCount = 0;
allArtifacts.map((artifact) => {
if (artifact.sync && artifact.source) {
enabledCount++;
if (artifact.type === 'subscription') {
const subName = artifact.source;
const sub = findByName(allSubs, subName);
@@ -635,13 +496,6 @@ async function syncArtifacts() {
}
});
if (enabledCount === 0) {
$.info(
`需同步的配置: ${enabledCount}, 总数: ${allArtifacts.length}`,
);
return;
}
if (subNames.length > 0) {
await Promise.all(
subNames.map(async (subName) => {
@@ -649,7 +503,6 @@ async function syncArtifacts() {
await produceArtifact({
type: 'subscription',
name: subName,
awaitCustomCache: true,
});
} catch (e) {
// $.error(`${e.message ?? e}`);
@@ -663,16 +516,6 @@ async function syncArtifacts() {
try {
if (artifact.sync && artifact.source) {
$.info(`正在同步云配置:${artifact.name}...`);
const useMihomoExternal =
artifact.platform === 'SurgeMac';
if (useMihomoExternal) {
$.info(
`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`,
);
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
@@ -680,7 +523,6 @@ async function syncArtifacts() {
produceOpts: {
'include-unsupported-proxy':
artifact.includeUnsupportedProxy,
useMihomoExternal,
},
});
@@ -690,47 +532,27 @@ async function syncArtifacts() {
files[encodeURIComponent(artifact.name)] = {
content: output,
};
valid.push(artifact.name);
}
} catch (e) {
$.error(
`生成同步配置 ${artifact.name} 发生错误: ${
e.message ?? e
}`,
`同步配置 ${artifact.name} 发生错误: ${e.message ?? e}`,
);
invalid.push(artifact.name);
}
}),
);
$.info(`${valid.length} 个同步配置生成成功: ${valid.join(', ')}`);
$.info(`${invalid.length} 个同步配置生成失败: ${invalid.join(', ')}`);
if (valid.length === 0) {
if (invalid.length > 0) {
throw new Error(
`同步配置 ${invalid.join(', ')} 生成失败 详情请查看日志`,
`同步配置 ${invalid.join(', ')} 发生错误 详情请查看日志`,
);
}
const resp = await syncToGist(files);
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
for (const artifact of allArtifacts) {
if (
artifact.sync &&
artifact.source &&
valid.includes(artifact.name)
) {
if (artifact.sync) {
artifact.updated = new Date().getTime();
// extract real url from gist
let files = body.files;
@@ -758,17 +580,9 @@ async function syncArtifacts() {
}
$.write(allArtifacts, ARTIFACTS_KEY);
$.info('上传配置成功');
if (invalid.length > 0) {
throw new Error(
`同步配置成功 ${valid.length} 个, 失败 ${invalid.length} 个, 详情请查看日志`,
);
} else {
$.info(`同步配置成功 ${valid.length}`);
}
$.info('全部订阅同步成功');
} catch (e) {
$.error(`同步配置失败,原因:${e.message ?? e}`);
$.error(`同步订阅失败,原因:${e.message ?? e}`);
throw e;
}
}
@@ -778,7 +592,7 @@ async function syncAllArtifacts(_, res) {
await syncArtifacts();
success(res);
} catch (e) {
$.error(`同步配置失败,原因:${e.message ?? e}`);
$.error(`同步订阅失败,原因:${e.message ?? e}`);
failed(
res,
new InternalServerError(
@@ -792,6 +606,7 @@ async function syncAllArtifacts(_, res) {
async function syncArtifact(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`开始同步远程配置 ${name}...`);
const allArtifacts = $.read(ARTIFACTS_KEY);
const artifact = findByName(allArtifacts, name);
@@ -823,18 +638,12 @@ async function syncArtifact(req, res) {
}
try {
const useMihomoExternal = artifact.platform === 'SurgeMac';
if (useMihomoExternal) {
$.info(`手动指定了 target 为 SurgeMac, 将使用 Mihomo External`);
}
const output = await produceArtifact({
type: artifact.type,
name: artifact.source,
platform: artifact.platform,
produceOpts: {
'include-unsupported-proxy': artifact.includeUnsupportedProxy,
useMihomoExternal,
},
});
@@ -854,16 +663,6 @@ async function syncArtifact(req, res) {
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
delete body.history;
delete body.forks;
delete body.owner;
Object.values(body.files).forEach((file) => {
delete file.content;
});
$.info('上传配置响应:');
$.info(JSON.stringify(body, null, 2));
let files = body.files;
let isGitLab;
if (Array.isArray(files)) {

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { SETTINGS_KEY, FILES_KEY, MODULES_KEY } from '@/constants';
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
@@ -11,26 +11,15 @@ import {
validCheck,
} from '@/utils/flow';
import $ from '@/core/app';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
import { ProxyUtils } from '@/core/proxy-utils';
const clashPreprocessor = PROXY_PREPROCESSORS.find(
(processor) => processor.name === 'Clash Pre-processor',
);
const tasks = new Map();
export default async function download(
rawUrl = '',
rawUrl,
ua,
timeout,
customProxy,
proxy,
skipCustomCache,
awaitCustomCache,
noCache,
preprocess,
) {
let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, '');
@@ -52,136 +41,50 @@ export default async function download(
}
}
}
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const {
defaultProxy,
defaultUserAgent,
defaultTimeout,
cacheThreshold: defaultCacheThreshold,
} = $.read(SETTINGS_KEY);
const cacheThreshold = defaultCacheThreshold || 1024;
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
if ($arguments?.cacheKey === true) {
$.error(`使用自定义缓存时 cacheKey 的值不能为空`);
$arguments.cacheKey = undefined;
}
const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined;
if (customCacheKey && !skipCustomCache) {
const customCached = $.read(customCacheKey);
const cached = resourceCache.get(id);
if (!noCache && !$arguments?.noCache && cached) {
const cached = $.read(customCacheKey);
if (cached) {
$.info(
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试更新缓存`,
);
download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
);
return cached;
}
if (customCached) {
if (awaitCustomCache) {
$.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
try {
await download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
undefined,
undefined,
preprocess,
);
} catch (e) {
$.error(
`乐观缓存: URL ${url} 更新缓存发生错误 ${
e.message ?? e
}`,
);
$.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
resourceCache.set(id, customCached);
}
} else {
$.info(
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
);
download(
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
ua,
timeout,
proxy,
true,
undefined,
undefined,
preprocess,
).catch((e) => {
$.error(
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
e.message ?? e
}`,
);
});
}
return customCached;
}
}
const downloadUrlMatch = url
.split('#')[0]
.match(/^\/api\/(file|module)\/(.+)/);
if (downloadUrlMatch) {
let type = '';
try {
type = downloadUrlMatch?.[1];
let name = downloadUrlMatch?.[2];
if (name == null) {
throw new Error(`本地 ${type} URL 无效: ${url}`);
}
name = decodeURIComponent(name);
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
const item = findByName($.read(key), name);
if (!item) {
throw new Error(`找不到 ${type}: ${name}`);
}
// 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}`);
// }
if (type === 'module') {
return item.content;
} else {
return await produceArtifact({
type: 'file',
name,
});
}
} catch (err) {
$.error(
`Error when loading ${type}: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法加载 ${type}: ${url}`);
}
} else if (url?.startsWith('/')) {
try {
const fs = eval(`require("fs")`);
return fs.readFileSync(url.split('#')[0], 'utf8');
} catch (err) {
$.error(
`Error when reading local file: ${
url.split('#')[0]
}.\n Reason: ${err}`,
);
throw new Error(`无法从该路径读取文本内容: ${url}`);
}
}
// 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);
}
@@ -201,53 +104,30 @@ export default async function download(
// try to find in app cache
const cached = resourceCache.get(id);
if (!noCache && !$arguments?.noCache && cached) {
$.info(`使用缓存: ${url}, ${userAgent}`);
if (!$arguments?.noCache && cached && !skipCustomCache) {
$.info(`使用缓存: ${url}`);
result = cached;
if (customCacheKey) {
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
$.write(cached, customCacheKey);
}
} else {
const insecure = $arguments?.insecure
? isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
$.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nInsecure: ${!!insecure}\nPreprocess: ${preprocess}\nURL: ${url}`,
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
);
try {
let { body, headers, statusCode } = await http.get({
const { body, headers } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
$.info(`statusCode: ${statusCode}`);
if (statusCode < 200 || statusCode >= 400) {
throw new Error(`statusCode: ${statusCode}`);
}
if (headers) {
const flowInfo = getFlowField(headers);
if (flowInfo) {
headersResourceCache.set(id, flowInfo);
headersResourceCache.set(url, flowInfo);
}
}
if (body.replace(/\s/g, '').length === 0)
throw new Error(new Error('远程资源内容为空'));
if (preprocess) {
try {
if (clashPreprocessor.test(body)) {
body = clashPreprocessor.parse(body, true);
}
} catch (e) {
$.error(`Clash Pre-processor error: ${e}`);
}
}
let shouldCache = true;
if (cacheThreshold) {
const size = body.length / 1024;
@@ -260,20 +140,6 @@ export default async function download(
shouldCache = false;
}
}
if (preprocess) {
try {
const proxies = ProxyUtils.parse(body);
if (!Array.isArray(proxies) || proxies.length === 0) {
$.error(`URL ${url} 不包含有效节点, 不缓存`);
shouldCache = false;
}
} catch (e) {
$.error(
`URL ${url} 尝试解析节点失败 ${e.message ?? e}, 不缓存`,
);
shouldCache = false;
}
}
if (shouldCache) {
resourceCache.set(id, body);
if (customCacheKey) {
@@ -311,7 +177,6 @@ export default async function download(
$arguments.flowUserAgent,
undefined,
proxy,
$arguments.flowUrl,
),
),
);
@@ -322,25 +187,3 @@ export default async function download(
}
return result;
}
export async function downloadFile(url, file) {
const undici = eval("require('undici')");
const fs = eval("require('fs')");
const { pipeline } = eval("require('stream/promises')");
const { Agent, interceptors, request } = undici;
$.info(`Downloading file...\nURL: ${url}\nFile: ${file}`);
const { body, statusCode } = await request(url, {
dispatcher: new Agent().compose(
interceptors.redirect({
maxRedirections: 3,
throwOnRedirect: true,
}),
),
});
if (statusCode !== 200)
throw new Error(`Failed to download file from ${url}`);
const fileStream = fs.createWriteStream(file);
await pipeline(body, fileStream);
$.info(`File downloaded from ${url} to ${file}`);
return file;
}

View File

@@ -24,7 +24,6 @@ if (isLanceX) backend = 'LanceX';
if (isGUIforCores) backend = 'GUI.for.Cores';
let meta = {};
let feature = {};
try {
if (typeof $environment !== 'undefined') {
@@ -64,6 +63,5 @@ try {
export default {
backend,
version: substoreVersion,
feature,
meta,
};

View File

@@ -1,35 +1,17 @@
import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache';
export function getFlowField(headers) {
const keys = Object.keys(headers);
let sub = '';
let webPage = '';
for (let k of keys) {
const lower = k.toLowerCase();
if (lower === 'subscription-userinfo') {
sub = headers[k];
} else if (lower === 'profile-web-page-url') {
webPage = headers[k];
}
}
return `${sub || ''}${
webPage ? `; app_url=${encodeURIComponent(webPage)}` : ''
}`;
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
export async function getFlowHeaders(
rawUrl,
ua,
timeout,
customProxy,
flowUrl,
) {
let url = flowUrl || rawUrl || '';
export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
let url = rawUrl;
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
@@ -49,129 +31,80 @@ export async function getFlowHeaders(
}
}
}
if ($arguments?.noFlow || !/^https?/.test(url)) {
if ($arguments?.noFlow) {
return;
}
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const insecure = $arguments?.insecure
? $.env.isNode
? { strictSSL: false }
: { insecure: true }
: undefined;
const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultFlowUserAgent || 'clash';
const requestTimeout = timeout || defaultTimeout || 8000;
const id = hex_md5(userAgent + url);
const cached = headersResourceCache.get(id);
const cached = headersResourceCache.get(url);
let flowInfo;
if (!$arguments?.noCache && cached) {
$.info(`使用缓存的流量信息: ${url}, ${userAgent}`);
// $.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();
if (flowUrl) {
try {
$.info(
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
}`,
);
const { body } = await http.get({
url: flowUrl,
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = body;
} else {
try {
$.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.head({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
} catch (e) {
$.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}: ${
e.message ?? e
}`,
);
}
if (!flowInfo) {
$.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}, Insecure: ${!!insecure}, Proxy: ${proxy}`,
);
const { headers } = await http.get({
url: url
.split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)[0],
headers: {
'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
...(insecure ? insecure : {}),
});
flowInfo = getFlowField(headers);
}
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) {
flowInfo = flowInfo.trim();
}
if (flowInfo) {
headersResourceCache.set(id, flowInfo);
headersResourceCache.set(url, flowInfo);
}
}
@@ -202,35 +135,14 @@ export function parseFlowHeaders(flowHeaders) {
? Number(expireMatch[1] + expireMatch[2])
: undefined;
const remainingDaysMatch = flowHeaders.match(/reset_day=([0-9]+)/);
const remainingDays = remainingDaysMatch
? Number(remainingDaysMatch[1])
: undefined;
const appUrlMatch = flowHeaders.match(/app_url=(.*?)\s*?(;|$)/);
const appUrl = appUrlMatch ? decodeURIComponent(appUrlMatch[1]) : undefined;
const planNameMatch = flowHeaders.match(/plan_name=(.*?)\s*?(;|$)/);
const planName = planNameMatch
? decodeURIComponent(planNameMatch[1])
: undefined;
return {
expires,
total,
usage: { upload, download },
remainingDays,
appUrl,
planName,
};
return { expires, total, usage: { upload, download } };
}
export function flowTransfer(flow, unit = 'B') {
const unitList = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let unitIndex = unitList.indexOf(unit);
return flow < 1024 || unitIndex === unitList.length - 1
? { value: (Math.round(flow * 100) / 100).toString(), unit: unit }
? { value: flow.toFixed(1), unit: unit }
: flowTransfer(flow / 1024, unitList[++unitIndex]);
}
@@ -313,62 +225,3 @@ export function getRmainingDays(opt = {}) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}
export function normalizeFlowHeader(flowHeaders) {
try {
// 使用 Map 保持顺序并处理重复键
const kvMap = new Map();
flowHeaders
.split(';')
.map((p) => p.trim())
.filter(Boolean)
.forEach((pair) => {
const eqIndex = pair.indexOf('=');
if (eqIndex === -1) return;
const key = pair.slice(0, eqIndex).trim();
const encodedValue = pair.slice(eqIndex + 1).trim();
// 只保留第一个出现的 key
if (!kvMap.has(key)) {
try {
// 解码 URI 组件并保留原始值作为 fallback
let decodedValue = decodeURIComponent(encodedValue);
if (
['upload', 'download', 'total', 'expire'].includes(
key,
)
) {
try {
decodedValue = Number(decodedValue).toFixed(0);
if (
['expire'].includes(key) &&
decodedValue <= 0
) {
decodedValue = '';
}
} catch (e) {
$.error(
`Failed to convert value for key "${key}=${encodedValue}": ${
e.message ?? e
}`,
);
}
}
kvMap.set(key, decodedValue);
} catch (e) {
kvMap.set(key, encodedValue);
}
}
});
// 拼接标准化字符串
return Array.from(kvMap.entries())
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
.join('; ');
} catch (e) {
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
return flowHeaders;
}
}

View File

@@ -17,10 +17,7 @@ const ISOFlags = {
'🇧🇪': ['BE', 'BEL'],
'🇧🇬': ['BG', 'BGR'],
'🇧🇭': ['BH', 'BHR'],
'🇧🇴': ['BO', 'BOL'],
'🇧🇳': ['BN', 'BRN'],
'🇧🇷': ['BR', 'BRA'],
'🇧🇹': ['BT', 'BTN'],
'🇧🇾': ['BY', 'BLR'],
'🇨🇦': ['CA', 'CAN'],
'🇨🇭': ['CH', 'CHE'],
@@ -41,12 +38,9 @@ const ISOFlags = {
'🇬🇧': ['GB', 'GBR', 'UK'],
'🇬🇪': ['GE', 'GEO'],
'🇬🇷': ['GR', 'GRC'],
'🇬🇹': ['GT', 'GTM'],
'🇬🇺': ['GU', 'GUM'],
'🇭🇰': ['HK', 'HKG', 'HKT', 'HKBN', 'HGC', 'WTT', 'CMI'],
'🇭🇷': ['HR', 'HRV'],
'🇭🇺': ['HU', 'HUN'],
'🇮🇶': ['IQ', 'IRQ'], // 伊拉克
'🇯🇴': ['JO', 'JOR'],
'🇯🇵': ['JP', 'JPN', 'TYO'],
'🇰🇪': ['KE', 'KEN'],
@@ -63,15 +57,12 @@ const ISOFlags = {
'🇮🇷': ['IR', 'IRN'],
'🇮🇸': ['IS', 'ISL'],
'🇮🇹': ['IT', 'ITA'],
'🇱🇦': ['LA', 'LAO'],
'🇱🇰': ['LK', 'LKA'],
'🇱🇹': ['LT', 'LTU'],
'🇱🇺': ['LU', 'LUX'],
'🇱🇻': ['LV', 'LVA'],
'🇲🇦': ['MA', 'MAR'],
'🇲🇩': ['MD', 'MDA'],
'🇳🇬': ['NG', 'NGA'],
'🇲🇲': ['MM', 'MMR'],
'🇲🇰': ['MK', 'MKD'],
'🇲🇳': ['MN', 'MNG'],
'🇲🇴': ['MO', 'MAC', 'CTM'],
@@ -90,7 +81,6 @@ const ISOFlags = {
'🇵🇷': ['PR', 'PRI'],
'🇵🇹': ['PT', 'PRT'],
'🇵🇾': ['PY', 'PRY'],
'🇵🇬': ['PG', 'PNG'],
'🇷🇴': ['RO', 'ROU'],
'🇷🇸': ['RS', 'SRB'],
'🇷🇪': ['RE', 'REU'],
@@ -100,7 +90,6 @@ const ISOFlags = {
'🇸🇬': ['SG', 'SGP'],
'🇸🇮': ['SI', 'SVN'],
'🇸🇰': ['SK', 'SVK'],
'🇹🇬': ['TG', 'TGO'], // 多哥
'🇹🇭': ['TH', 'THA'],
'🇹🇳': ['TN', 'TUN'],
'🇹🇷': ['TR', 'TUR'],
@@ -151,10 +140,7 @@ export function getFlag(name) {
'🇧🇬': ['Bulgaria', '保加利亚', '保加利亞'],
'🇧🇭': ['Bahrain', '巴林'],
'🇧🇷': ['Brazil', '巴西', '圣保罗'],
'🇧🇳': ['Brunei', '文莱', '汶萊'],
'🇧🇾': ['Belarus', '白俄罗斯', '白俄'],
'🇧🇴': ['Bolivia', '玻利维亚'],
'🇧🇹': ['Bhutan', '不丹', '不丹王国'],
'🇨🇦': [
'Canada',
'加拿大',
@@ -165,7 +151,6 @@ export function getFlag(name) {
'滑铁卢',
'多伦多',
'Waterloo',
'Toronto',
],
'🇨🇭': ['Switzerland', '瑞士', '苏黎世', 'Zurich'],
'🇨🇱': ['Chile', '智利'],
@@ -205,8 +190,6 @@ export function getFlag(name) {
],
'🇬🇪': ['Georgia', '格鲁吉亚', '格魯吉亞'],
'🇬🇷': ['Greece', '希腊', '希臘'],
'🇬🇺': ['Guam', '关岛', '關島'],
'🇬🇹': ['Guatemala', '危地马拉'],
'🇭🇰': [
'Hongkong',
'香港',
@@ -222,7 +205,6 @@ export function getFlag(name) {
],
'🇭🇷': ['Croatia', '克罗地亚', '克羅地亞'],
'🇭🇺': ['Hungary', '匈牙利'],
'🇮🇶': ['Iraq', '伊拉克', '巴格达', 'Baghdad'], // 伊拉克
'🇯🇴': ['Jordan', '约旦'],
'🇯🇵': [
'Japan',
@@ -263,18 +245,15 @@ export function getFlag(name) {
'🇮🇪': ['Ireland', '爱尔兰', '愛爾蘭', '都柏林'],
'🇮🇱': ['Israel', '以色列'],
'🇮🇲': ['Isle of Man', '马恩岛', '馬恩島'],
'🇮🇳': ['India', '印度', '孟买', 'MFumbai', 'Mumbai'],
'🇮🇳': ['India', '印度', '孟买', 'MFumbai'],
'🇮🇷': ['Iran', '伊朗'],
'🇮🇸': ['Iceland', '冰岛', '冰島'],
'🇮🇹': ['Italy', '意大利', '義大利', '米兰', 'Nachash'],
'🇱🇰': ['Sri Lanka', '斯里兰卡', '斯里蘭卡'],
'🇱🇦': ['Laos', '老挝', '老撾'],
'🇱🇹': ['Lithuania', '立陶宛'],
'🇱🇺': ['Luxembourg', '卢森堡'],
'🇱🇻': ['Latvia', '拉脱维亚', 'Latvija'],
'🇲🇦': ['Morocco', '摩洛哥'],
'🇲🇩': ['Moldova', '摩尔多瓦', '摩爾多瓦'],
'🇲🇲': ['Myanmar', '缅甸', '緬甸'],
'🇳🇬': ['Nigeria', '尼日利亚', '尼日利亞'],
'🇲🇰': ['Macedonia', '马其顿', '馬其頓'],
'🇲🇳': ['Mongolia', '蒙古'],
@@ -282,14 +261,7 @@ export function getFlag(name) {
'🇲🇹': ['Malta', '马耳他'],
'🇲🇽': ['Mexico', '墨西哥'],
'🇲🇾': ['Malaysia', '马来', '馬來', '吉隆坡', '大馬'],
'🇳🇱': [
'Netherlands',
'荷兰',
'荷蘭',
'尼德蘭',
'阿姆斯特丹',
'Amsterdam',
],
'🇳🇱': ['Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇴': ['Norway', '挪威'],
'🇳🇵': ['Nepal', '尼泊尔'],
'🇳🇿': ['New Zealand', '新西兰', '新西蘭'],
@@ -297,10 +269,9 @@ export function getFlag(name) {
'🇵🇪': ['Peru', '秘鲁', '祕魯'],
'🇵🇭': ['Philippines', '菲律宾', '菲律賓'],
'🇵🇰': ['Pakistan', '巴基斯坦'],
'🇵🇱': ['Poland', '波兰', '波蘭', '华沙', 'Warsaw'],
'🇵🇱': ['Poland', '波兰', '波蘭'],
'🇵🇷': ['Puerto Rico', '波多黎各'],
'🇵🇹': ['Portugal', '葡萄牙'],
'🇵🇬': ['Papua New Guinea', '巴布亚新几内亚'],
'🇵🇾': ['Paraguay', '巴拉圭'],
'🇷🇴': ['Romania', '罗马尼亚'],
'🇷🇸': ['Serbia', '塞尔维亚'],
@@ -322,8 +293,8 @@ export function getFlag(name) {
'沪俄',
'Moscow',
],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特', 'Riyadh', '利雅得'],
'🇸🇪': ['Sweden', '瑞典', '斯德哥尔摩', 'Stockholm'],
'🇸🇦': ['Saudi', '沙特阿拉伯', '沙特'],
'🇸🇪': ['Sweden', '瑞典'],
'🇸🇬': [
'Singapore',
'新加坡',
@@ -341,10 +312,9 @@ export function getFlag(name) {
],
'🇸🇮': ['Slovenia', '斯洛文尼亚'],
'🇸🇰': ['Slovakia', '斯洛伐克'],
'🇹🇬': ['Togo', '多哥', '洛美', 'Lomé', 'Lome'], // 多哥
'🇹🇭': ['Thailand', '泰国', '泰國', '曼谷'],
'🇹🇳': ['Tunisia', '突尼斯'],
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔', 'Istanbul'],
'🇹🇷': ['Turkey', '土耳其', '伊斯坦布尔'],
'🇹🇼': [
'Taiwan',
'台湾',
@@ -359,7 +329,6 @@ export function getFlag(name) {
'台',
'臺',
'Taipei',
'Tai Wan',
],
'🇺🇦': ['Ukraine', '乌克兰', '烏克蘭'],
'🇺🇸': [
@@ -371,7 +340,6 @@ export function getFlag(name) {
'波特兰',
'达拉斯',
'俄勒冈',
'Oregon',
'凤凰城',
'费利蒙',
'硅谷',
@@ -385,17 +353,10 @@ export function getFlag(name) {
'沪美',
'哥伦布',
'纽约',
'New York',
'Los Angeles',
'San Jose',
'Sillicon Valley',
'Michigan',
'俄亥俄',
'Ohio',
'马纳萨斯',
'Manassas',
'弗吉尼亚',
'Virginia',
],
'🇺🇾': ['Uruguay', '乌拉圭'],
'🇻🇪': ['Venezuela', '委内瑞拉'],
@@ -456,12 +417,8 @@ export function getFlag(name) {
RegExp(`(^|[^a-zA-Z])${keyword}([^a-zA-Z]|$)`).test(name),
)
) {
const isCN2 =
flag == '🇨🇳' &&
RegExp(`(^|[^a-zA-Z])CN2([^a-zA-Z]|$)`).test(name);
if (!isCN2) {
return (Flag = flag);
}
//console.log(`ISOFlag = ${flag}`)
return (Flag = flag);
}
}
@@ -473,13 +430,6 @@ export function getISO(name) {
return ISOFlags[getFlag(name)]?.[0];
}
// remove flag
export function removeFlag(str) {
return str
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴‍☠️|🏳️‍🌈/g, '')
.trim();
}
export class MMDB {
constructor({ country, asn } = {}) {
if ($.env.isNode) {
@@ -488,21 +438,11 @@ export class MMDB {
const countryFile =
country || eval('process.env.SUB_STORE_MMDB_COUNTRY_PATH');
const asnFile = asn || eval('process.env.SUB_STORE_MMDB_ASN_PATH');
// $.info(
// `GeoLite2 Country MMDB: ${countryFile}, exists: ${fs.existsSync(
// countryFile,
// )}`,
// );
if (countryFile) {
this.countryReader = Reader.openBuffer(
fs.readFileSync(countryFile),
);
}
// $.info(
// `GeoLite2 ASN MMDB: ${asnFile}, exists: ${fs.existsSync(
// asnFile,
// )}`,
// );
if (asnFile) {
if (!fs.existsSync(asnFile))
throw new Error('GeoLite2 ASN MMDB does not exist');
@@ -516,7 +456,4 @@ export class MMDB {
ipaso(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemOrganization;
}
ipasn(ip) {
return this.asnReader?.asn(ip)?.autonomousSystemNumber;
}
}

View File

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

View File

@@ -1,4 +1,3 @@
import * as ipAddress from 'ip-address';
// source: https://stackoverflow.com/a/36760050
const IPV4_REGEX = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
@@ -47,113 +46,54 @@ function getPolicyDescriptor(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 = [];
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;
return function (array) {
var codePt, byte1;
var buffLen = array.length;
// result.length = 0;
result.length = 0;
// for (var i = 0; i < buffLen; ) {
// byte1 = array[i++];
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;
// }
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)),
// );
// }
result.push(
charCache[codePt] ||
(charCache[codePt] = charFromCodePt(codePt)),
);
}
// return result.join('');
// };
// })();
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function getRandomPort(portString) {
let portParts = portString.split(/,|\//);
let randomPart = portParts[Math.floor(Math.random() * portParts.length)];
if (randomPart.includes('-')) {
let [min, max] = randomPart.split('-').map(Number);
return getRandomInt(min, max);
} else {
return Number(randomPart);
}
}
function numberToString(value) {
return Number.isSafeInteger(value)
? String(value)
: BigInt(value).toString();
}
function isValidUUID(uuid) {
return (
typeof uuid === 'string' &&
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
uuid,
)
);
}
function formatDateTime(date, format = 'YYYY-MM-DD_HH-mm-ss') {
const d = date instanceof Date ? date : new Date(date);
if (isNaN(d.getTime())) {
return '';
}
const pad = (num) => String(num).padStart(2, '0');
const replacements = {
YYYY: d.getFullYear(),
MM: pad(d.getMonth() + 1),
DD: pad(d.getDate()),
HH: pad(d.getHours()),
mm: pad(d.getMinutes()),
ss: pad(d.getSeconds()),
};
return format.replace(
/YYYY|MM|DD|HH|mm|ss/g,
(match) => replacements[match],
);
}
return result.join('');
};
})();
export {
formatDateTime,
isValidUUID,
ipAddress,
isIPv4,
isIPv6,
isValidPortNumber,
@@ -161,8 +101,6 @@ export {
getIfNotBlank,
isPresent,
getIfPresent,
// utf8ArrayToStr,
utf8ArrayToStr,
getPolicyDescriptor,
getRandomPort,
numberToString,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ export default function express({ substore: $, port, host }) {
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept',
'X-Powered-By': 'Sub-Store',
};
// node support
@@ -17,14 +16,7 @@ export default function express({ substore: $, port, host }) {
const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`);
const app = express_();
const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb';
$.info(`[BACKEND] body JSON limit: ${limit}`);
app.use(
bodyParser.json({
verify: rawBodySaver,
limit,
}),
);
app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
);
@@ -36,12 +28,9 @@ export default function express({ substore: $, port, host }) {
// adapter
app.start = () => {
app.get('*', function (req, res) {
res.status(404).end();
});
const listener = app.listen(port, host, () => {
const { address, port } = listener.address();
$.info(`[BACKEND] listening on ${address}:${port}`);
$.info(`[BACKEND] ${address}:${port}`);
});
};
return app;
@@ -270,7 +259,7 @@ function extractURL(url) {
let hashes = url.slice(url.indexOf('?') + 1).split('&');
for (let i = 0; i < hashes.length; i++) {
const hash = hashes[i].split('=');
query[hash[0]] = decodeURIComponent(hash[1]);
query[hash[0]] = hash[1];
}
}
return {
@@ -294,7 +283,7 @@ function extractPathParams(pattern, path) {
while (path[j] !== '/' && j < path.length) {
val.push(path[j++]);
}
params[key.join('')] = decodeURIComponent(val.join(''));
params[key.join('')] = val.join('');
} else {
if (pattern[i] !== path[j]) {
return null;

View File

@@ -9,36 +9,7 @@ const isShadowRocket = 'undefined' !== typeof $rocket;
const isEgern = 'object' == typeof egern;
const isLanceX = 'undefined' != typeof $native;
const isGUIforCores = typeof $Plugins !== 'undefined';
import { Base64 } from 'js-base64';
function isPlainObject(obj) {
return (
obj !== null &&
typeof obj === 'object' &&
[null, Object.prototype].includes(Object.getPrototypeOf(obj))
);
}
function parseSocks5Uri(uri) {
// eslint-disable-next-line no-unused-vars
let [__, username, password, server, port, query, name] = uri.match(
/^socks5:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${uri}`);
throw new Error(`port is not present in line: ${uri}`);
}
return {
type: 5,
host: server,
port,
userId: username != null ? decodeURIComponent(username) : undefined,
password: password != null ? decodeURIComponent(password) : undefined,
};
}
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
this.name = name;
@@ -47,10 +18,6 @@ export class OpenAPI {
this.http = HTTP();
this.env = ENV();
if (isNode) {
const dotenv = eval(`require("dotenv")`);
dotenv.config();
}
this.node = (() => {
if (isNode) {
const fs = eval("require('fs')");
@@ -91,64 +58,29 @@ export class OpenAPI {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
let rootPath = `${basePath}/root.json`;
const backupRootPath = `${basePath}/root_${Date.now()}.json`;
this.log(`Root path: ${rootPath}`);
if (this.node.fs.existsSync(rootPath)) {
try {
this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
} catch (e) {
this.node.fs.copyFileSync(rootPath, backupRootPath);
this.error(
`Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`,
);
}
}
if (!isPlainObject(this.root)) {
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'w',
flag: 'wx',
});
this.root = {};
} else {
this.root = JSON.parse(
this.node.fs.readFileSync(`${rootPath}`),
);
}
// create a json file with the given name if not exists
let fpath = `${basePath}/${this.name}.json`;
const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
this.log(`Data path: ${fpath}`);
if (this.node.fs.existsSync(fpath)) {
try {
this.cache = JSON.parse(
this.node.fs.readFileSync(`${fpath}`, 'utf-8'),
);
if (!isPlainObject(this.cache))
throw new Error('Invalid Data');
} catch (e) {
try {
const str = Base64.decode(
this.node.fs.readFileSync(`${fpath}`, 'utf-8'),
);
this.cache = JSON.parse(str);
this.node.fs.writeFileSync(fpath, str, {
flag: 'w',
});
if (!isPlainObject(this.cache))
throw new Error('Invalid Data');
} catch (e) {
this.node.fs.copyFileSync(fpath, backupPath);
this.error(
`Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`,
);
}
}
}
if (!isPlainObject(this.cache)) {
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'w',
flag: 'wx',
});
this.cache = {};
} else {
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
}
}
}
@@ -405,130 +337,42 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
opts: options.opts,
});
} else if (isLoon || isSurge || isNode) {
worker = new Promise(async (resolve, reject) => {
const body = options.body;
worker = new Promise((resolve, reject) => {
const request = isNode
? eval("require('request')")
: $httpClient;
const opts = JSON.parse(JSON.stringify(options));
opts.body = body;
opts.timeout = opts.timeout || 8000;
if (opts.timeout) {
if (!isNode && opts.timeout) {
opts.timeout++;
if (isNaN(opts.timeout)) {
opts.timeout = 8000;
}
if (!isNode) {
let unit = 'ms';
// 这些客户端单位为 s
if (isSurge || isStash || isShadowRocket) {
opts.timeout = Math.ceil(opts.timeout / 1000);
unit = 's';
}
// Loon 为 ms
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
let unit = 'ms';
// 这些客户端单位为 s
if (isSurge || isStash || isShadowRocket) {
opts.timeout = Math.ceil(opts.timeout / 1000);
unit = 's';
}
// Loon 为 ms
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
}
if (isNode) {
const undici = eval("require('undici')");
const { socksDispatcher } = eval("require('fetch-socks')");
const {
ProxyAgent,
EnvHttpProxyAgent,
request,
interceptors,
} = undici;
const agentOpts = {
connect: {
rejectUnauthorized:
opts.strictSSL === false ||
opts.insecure === true
? false
: true,
},
bodyTimeout: opts.timeout,
headersTimeout: opts.timeout,
};
try {
const url = new URL(opts.url);
if (url.username || url.password) {
opts.headers = {
...(opts.headers || {}),
Authorization: `Basic ${Buffer.from(
`${url.username || ''}:${
url.password || ''
}`,
).toString('base64')}`,
};
}
let dispatcher;
if (!opts.proxy) {
const allProxy =
eval('process.env.all_proxy') ||
eval('process.env.ALL_PROXY');
if (allProxy && /^socks5:\/\//.test(allProxy)) {
opts.proxy = allProxy;
}
}
if (opts.proxy) {
if (/^socks5:\/\//.test(opts.proxy)) {
dispatcher = socksDispatcher(
parseSocks5Uri(opts.proxy),
agentOpts,
);
} else {
dispatcher = new ProxyAgent({
...agentOpts,
uri: opts.proxy,
});
}
} else {
dispatcher = new EnvHttpProxyAgent(agentOpts);
}
const response = await request(opts.url, {
...opts,
method: method.toUpperCase(),
dispatcher: dispatcher.compose(
interceptors.redirect({
maxRedirections: 3,
throwOnMaxRedirects: true,
}),
),
});
resolve({
statusCode: response.statusCode,
headers: response.headers,
body:
opts.encoding === null
? await response.body.arrayBuffer()
: await response.body.text(),
});
} catch (e) {
reject(e);
}
} else {
$httpClient[method.toLowerCase()](
opts,
(err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
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,
});
},
);
}
if (err) reject(err);
else
resolve({
statusCode: response.status || response.statusCode,
headers: response.headers,
body,
});
});
});
} else if (isGUIforCores) {
worker = new Promise(async (resolve, reject) => {
@@ -538,13 +382,6 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
url: options.url,
headers: options.headers,
body: options.body,
autoTransformBody: false,
options: {
Proxy: options.proxy,
Timeout: options.timeout
? options.timeout / 1000
: 15,
},
});
resolve({
statusCode: response.status,

View File

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

View File

@@ -14,7 +14,7 @@ DOMAIN,sub-store.vercel.app,PROXY
hostname=sub.store
[Script]
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
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 "55 23 * * *" script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync
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

View File

@@ -2,6 +2,6 @@
"name": "Sub-Store",
"description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'",
"task": [
"55 23 * * * https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
"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,4 +1,4 @@
hostname=sub.store
^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
^https?:\/\/sub\.store url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js

View File

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

View File

@@ -25,13 +25,13 @@ cron:
script-providers:
sub-store-0:
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js
interval: 86400
sub-store-1:
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js
interval: 86400
cron-sync-artifacts:
url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js
interval: 86400

View File

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

View File

@@ -7,7 +7,7 @@ hostname = %APPEND% sub.store
[Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store 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://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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

View File

@@ -6,7 +6,7 @@
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://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store 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://raw.githubusercontent.com/sub-store-org/Sub-Store/release/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

View File

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

40
nginx/front.conf Normal file
View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB