1
0
mirror of https://github.com/bestnite/sub2clash.git synced 2026-06-18 03:58:16 +00:00

Compare commits

...

176 Commits

Author SHA1 Message Date
nite 7301c3d57d fix: use direct model import in convert handler 2026-04-26 16:43:13 +10:00
nite 9d23b11751 refactor: preserve template yaml structure 2026-04-25 23:22:36 +10:00
nite 2d863752b1 fix: use udp config 2026-01-03 22:43:31 +08:00
nite 9725a05c35 mod: model 2025-12-08 02:48:01 +08:00
nite 12de56d275 add: tuic protocol 2025-12-04 17:08:21 +08:00
nite f16779b441 fix: update short link 2025-12-02 23:16:10 +08:00
nite 516657f849 refactor(frontend): Extract short link UI into dedicated component 2025-10-20 16:45:42 +11:00
nite 800c5ff7f1 workflow 2025-10-19 04:16:31 +11:00
nite 892fa7ce41 workflow 2025-10-19 03:33:06 +11:00
nite 007093ac48 workflow 2025-10-19 03:28:27 +11:00
nite ed479b7efa workflow: pass version arg to builder 2025-10-19 03:23:43 +11:00
nite dac4760289 workflow 2025-10-19 03:17:08 +11:00
nite 86b74f30e7 Refactor(frontend): Refactor frontend using Lit
Refactor(database): use gorm+sqlite instead of bbolt
Feat: Add delete short link functionality
Fix: Load correct configuration template during meta config conversion
2025-10-19 03:13:10 +11:00
nite 1e8a79c2d2 #69 2025-10-17 18:13:49 +11:00
nite be656cca57 workflow 2025-10-15 17:33:15 +11:00
nite 23a85f573b refactor(template): Enhance template loading security and error messages 2025-10-15 16:40:07 +11:00
nite fce75baed4 docs: modify README 2025-10-15 15:56:23 +11:00
nite f5686561f9 modify base64 decode func in BuildSub uses std encoder 2025-07-28 12:12:15 +00:00
nite 83c24170a0 Update API documentation to reflect changes in configuration format for GET /convert/:config, including detailed descriptions of new parameters and structures for config, RuleProvider, and Rule. 2025-07-22 04:19:26 +00:00
nite 80d91efca4 Refactor subscription handling by removing SubConfig model, updating BuildSub function to use ConvertConfig, and enhancing Base64 decoding across parsers. Update routes and frontend to support new configuration format. 2025-07-22 04:09:00 +00:00
nite 83a728a415 Implement YAML unmarshalling for various proxy types and update SOCKS parser to support "socks5" prefix. 2025-07-15 20:14:37 +08:00
nite a178d06248 Merge pull request #67 from HaTiWinter/main
修复 GetRawConfHandler 中短链的构建问题 | Fix URL construction in GetRawConfHandler
2025-07-13 22:49:33 +08:00
HaTiWinter 99d36d93d8 fix URL construction in GetRawConfHandler 2025-07-13 15:57:46 +08:00
nite 0a9892503d u 2025-07-05 22:57:46 +08:00
nite ff81d03492 u 2025-07-05 22:54:02 +08:00
nite 0fa95888cb Fix URL construction in GetRawConfHandler to ensure proper HTTP scheme is used. 2025-07-01 02:22:46 +08:00
nite b44703fa0f Enhance Trojan, Vless, and Vmess parsers. 2025-07-01 02:06:33 +08:00
nite b256c5e809 Enhance Base64 validation in isLikelyBase64 function to include UTF-8 check and improve decoding logic. 2025-06-26 10:58:56 +08:00
nite 307c36aa8d Refactor Base64 validation in isLikelyBase64 function to remove unnecessary suffix check. 2025-06-26 09:51:39 +08:00
nite 06da2e91c2 Update index.html to set default option for Clash.Meta and improve user-agent label and placeholder text for clarity. 2025-06-26 09:04:53 +08:00
nite 33d37e631b Fix subscription user info header handling in SubHandler to only set header on successful fetch 2025-06-26 10:55:21 +10:00
nite c1012750ff add tests 2025-06-12 19:33:14 +10:00
nite 69deed91df Refactor Shadowsocks and Socks parsers to simplify username and password extraction 2025-06-12 15:48:59 +10:00
nite 6a780a5e27 Refactor Shadowsocks and Socks parsers to improve username and password handling 2025-06-12 15:41:56 +10:00
nite f88ae52a29 Unify parts of the model with MetaCubeX. 2025-06-12 14:19:00 +10:00
nite 0c8bbac2e6 Update README.md to modify contributor image link format for improved clarity. 2025-06-12 13:44:27 +10:00
nite c4554d9030 Remove .all-contributorsrc file and update README.md to display contributor statistics using an image link. 2025-06-12 13:43:17 +10:00
nite 8db2c46bf0 update regex to dynamically include all supported prefixes for improved proxy parsing. 2025-06-12 13:40:30 +10:00
nite 8b3ae45623 Merge pull request #64 from beck-8/fix/socks
fix reg socks
2025-06-12 13:35:47 +10:00
nite cdc8ef7e32 Update README.md to include contributor section 2025-06-12 13:30:46 +10:00
beck-8 62229fba97 fix reg socks 2025-06-12 10:41:59 +08:00
nite b80afd97f1 Refactor error handling 2025-06-12 11:33:13 +10:00
nite 2a042968d0 Update .gitignore to include .vscode directory and remove .vscode/launch.json file 2025-06-12 10:28:33 +10:00
nite cdf69ce65f Refactor proxy structure and parser implementations to streamline protocol handling; remove unused marshaler interfaces and improve YAML serialization for various proxy types. 2025-06-12 10:27:22 +10:00
nite 2d8508f390 Move HTTP, gRPC, Reality, and WebSocket options to proxy.go refactor VmessJson type to use 'any' for flexible types 2025-06-12 09:22:23 +10:00
nite 73616c98a3 fix Dockerfile 2025-06-12 03:14:08 +10:00
nite 44163d30e1 Update index.html to use local Bootstrap and Axios files instead of CDN links 2025-06-12 03:04:19 +10:00
nite a2e97aaa01 remove toml and ini config type 2025-06-12 02:55:50 +10:00
nite 4f3c2bb280 modify ProxyParser interface 2025-06-12 02:48:59 +10:00
nite 1b662de245 Update README.md 2025-06-12 02:28:07 +10:00
nite 4a9297f4a3 mod example config and compose.yaml 2025-06-12 02:24:18 +10:00
nite fcb1358846 Merge branch 'main' of https://github.com/bestnite/sub2clash 2025-06-12 02:21:21 +10:00
nite da9a17201b refactor 2025-06-12 02:17:31 +10:00
nite 5b7a94e65c Update docker.yml 2025-05-31 17:13:20 +10:00
nite b5fcbab1a5 Merge pull request #63 from pingchieh/main
添加Anytls协议
2025-05-28 14:43:29 +10:00
Chieh 0b8299f432 feat: 返回头中增加订阅信息 2025-05-26 11:18:33 +00:00
Chieh 84c5a6e5f4 fix: RuleProvider增加Format 2025-05-26 11:18:33 +00:00
Chieh 06887d91ac feat: 添加 Anytls 代理支持 2025-05-26 11:18:33 +00:00
nite db00433931 Merge pull request #59 from timerzz/main
fix 修复Get函数重试结束后,返回nil,nil的bug
2025-03-24 01:12:06 +11:00
timerzz 5b3a12f00d fix 修复Get函数重试结束后,返回nil,nil的bug 2025-03-23 21:27:05 +08:00
nite d4d7010d8f Merge pull request #56 from hausen1012/main
add:生成短链支持自定义id
2025-03-10 12:28:37 +11:00
hz 2331cd4d18 modify: 短链和密码修改为后端生成 2025-03-08 19:02:23 +08:00
hz 88d8653ab5 fix:修复ua标识等两个前端未正常回显问题 2025-03-07 17:41:42 +08:00
hz cc0b73d7a4 add:生成短链支持自定义id 2025-03-07 17:30:28 +08:00
nite b57e4cf49f fix ua form 2025-03-05 19:24:48 +11:00
Nite07 cc80e237d6 Merge pull request #48 from 96368a/main
 增加socks5协议支持
2024-11-13 10:31:41 +08:00
96368a fefb4b895a 增加socks5协议支持 2024-11-13 09:21:31 +08:00
nite 66f214ae10 try to fix ss parser 2024-11-06 18:43:48 +08:00
nite f7dc78aabc 🐛 2024-10-19 15:39:20 +08:00
nite 6bb2d16e4b 🐛 #46 2024-10-09 11:08:24 +08:00
nite 98ef93c7bb 🐛 #42 2024-10-08 10:05:13 +08:00
nite 6e09c44d17 🐛 #43 2024-10-08 10:01:09 +08:00
nite 42fd251eb5 #42 2024-10-07 16:34:56 +08:00
nite e504a6cca4 🎨 Refactor package for public import 2024-09-17 13:10:13 +08:00
nite 3cfa4bdf24 ️ Improve 2024-08-11 23:55:47 +08:00
nite dedbf2bc03 🐛 fix #37 2024-08-04 12:59:55 +08:00
Nite07 7158ad467a Merge pull request #35 from arusuki/main
🔧  customize HTTP user-agent for fetching subscription from API.
2024-08-04 12:53:06 +08:00
tao-lintian fd22cd1499 🔧 customize HTTP user-agent for fetching subscription from API. 2024-07-15 21:01:22 +08:00
nite 0946412ea7 🐛 #31 2024-05-21 15:28:09 +08:00
nite 98bca0a7ac 🐛 Cannot parse custom rules 2024-05-21 15:00:19 +08:00
nite 036907fba4 🐛 Parse short link 2024-05-09 12:58:33 +08:00
nite 3e720ec14d 🐛 Fix vless parser lost fingerprint 2024-05-06 02:22:44 +08:00
nite b73a02bdbf 🐛 Fix vless parser cannot correctly parse some reality/grpc fields 2024-05-05 22:46:01 +08:00
nite 25e47453cb 🔥 Remove test dir, verison length limit 2024-05-04 22:30:46 +08:00
nite 3330412243 🔧 Update docker workflow 2024-04-26 00:04:17 +08:00
nite 35deaa015f 🔥 Deprecate Woodpecker CI in favor of GitHub Actions 2024-04-25 22:23:33 +08:00
nite ddd297492c ⬆️ Upgrade dependencies 2024-04-24 13:05:10 +08:00
nite effd22c750 ♻️ Refactor logger
📝 Update README
2024-04-24 13:01:22 +08:00
nite 566965bb6a ♻️ Migrate from gorm/sqlite to boltdb 2024-04-24 12:51:37 +08:00
nite 3d3b4e0bea 🔧 Update workflows 2024-04-23 17:28:05 +08:00
nite b86aa2559a 🔧 Remove builder for arm 2024-04-23 15:03:21 +08:00
nite 4a2fa21a0a 🔧 Update docker-compose.yml 2024-04-23 14:54:10 +08:00
nite 3b8352a34f 🔧 update docker and goreleaser configrations 2024-04-23 14:49:31 +08:00
nite ac4ad3c8aa ♻️ Refactor code
🔥 Remove update detection
2024-04-23 14:47:53 +08:00
nite ebc91d8aad ♻️ Refactor parsers 2024-04-23 14:39:16 +08:00
nite 48dece2a51 🐛 Fix trojan parser missing fields 2024-04-23 13:36:33 +08:00
nite aa9e102a81 fix: hy2 解析缺少 name 字段 2024-04-21 00:23:29 +08:00
nite faaf5c366a mod 2024-04-18 00:15:45 +08:00
nite 4384e56cc6 mod 2024-04-17 23:08:26 +08:00
nite abbd7b8b19 mod 2024-04-17 22:55:04 +08:00
nite b687acb94c update 2024-04-17 21:52:03 +08:00
nite 7748407583 fix: goreleaser 2024-03-13 17:44:21 +08:00
Nite07 822793b085 Merge pull request #23 from nitezs/dev
Dev
2024-03-13 17:34:19 +08:00
nite 3d058066b9 update: 补全短链解析配置 2024-03-13 16:57:32 +08:00
nite 1c5e17a4ab feat: 解析短链 2024-03-13 16:28:40 +08:00
nite 867da56f45 fix: workflow 2024-03-13 13:47:53 +08:00
nite 14c3b97ed2 feat: 修改短链
update: dockerfile,workflow
2024-03-13 13:30:45 +08:00
nite 1d9de31f47 fix: vless ws 解析缺失 TLS 字段 2024-03-12 21:46:07 +08:00
Nite07 0681f599f1 Merge pull request #20 from QiChaiQiChai/dev
fix: hysteria 缺失参数 @QiChaiQiChai 
update: 策略组 icon 支持 @QiChaiQiChai
2024-03-11 12:40:29 +08:00
Qi Chai 68c64ad867 Update proxy_hysteria.go 2024-03-11 12:30:16 +08:00
Qi Chai 18b6a81896 修复hysterica节点配置中部分字段的识别 2024-03-11 12:26:42 +08:00
Qi Chai e5c3852f06 修复hysterica类型代理配置中部分字段的识别 2024-03-11 12:24:21 +08:00
Qi Chai ff73da3a4c Update proxy_group.go 增加对proxies group中icon字段的支持 2024-03-11 12:20:19 +08:00
nite 916670cf68 fix: vless 未解析 short-id 2024-03-10 13:56:22 +08:00
nite 94a320a682 feat: 增加不输出国家策略组功能 2024-03-10 13:26:35 +08:00
nite 65dccc3b51 fix: 合并完整配置模板 2024-03-10 11:34:52 +08:00
Nite07 c159f2b417 Merge pull request #18 from nitezs/dev
v0.0.8
2024-03-09 17:42:59 +08:00
nite fd7c460f95 feat: 增加 Hysteria 节点支持 2024-03-09 17:17:57 +08:00
nite 68269fd499 feat: 增加 Hysteria 节点支持 2024-03-09 17:01:52 +08:00
nite d8a81e44b6 fix: 修复重名节点没有正确重命名的问题 2024-03-09 15:58:16 +08:00
nite 70f5c63fe2 fix: 修复 vmess/vless 节点名称未URL解码的问题 2024-03-09 15:39:47 +08:00
nite 679102fa3b fix: 修复 vmes/vless 解析问题 2024-03-09 15:33:26 +08:00
Nite07 3309a0c208 Merge pull request #14 from nitezs/dev
Dev
2024-02-19 13:12:12 +08:00
nite f51958bd5b update: 增加警告提示 2024-02-19 12:29:52 +08:00
Nite07 9860c3fa53 Merge pull request #13 from remann2/dev
fix: reality parsing
2024-02-19 11:45:30 +08:00
Zongxin Gan b053281790 fix reality parsing 2024-02-19 00:31:56 +08:00
nite fd8164b08e update: 增加警告提示 2024-02-15 15:58:08 +08:00
nite 3616ae870a fix: 修复无法加载本地模板的问题 2024-02-15 15:35:07 +08:00
nite 73e94ad856 Reordered import statements across multiple files
for consistency
2023-11-03 02:35:30 +08:00
nite d0696aad5b feat: 增加hysteria2协议的支持 2023-10-31 15:14:29 +08:00
nite 50877b1691 fix: 修复模板中非select、url-test策略组被输出为null的问题 2023-09-29 15:58:38 +08:00
Nite07 773424cdb0 v0.0.6
feat: webui解析url到页面
feat: 增加输出NodeList选项
feat: 增加将订阅名称添加到节点名中的功能
feat: 增加地区模板变量
fix: 修复当base64字符串长度不为4的倍数时,解码失败的问题
fix: 修复vmess配置不规范导致无法解析的问题
update: 提高匹配国家名称的正确率
2023-09-28 10:08:53 +08:00
nite edfd70a77d modify 2023-09-27 14:54:53 +08:00
nite 6f075ea44e feat: 增加将订阅名称添加到节点名中的功能
feat: 增加地区模板变量
2023-09-25 23:58:13 +08:00
nite 6a5cfd35c9 feat: 增加输出NodeList选项 2023-09-25 15:43:21 +08:00
nite ad7d2b98f6 fix: 修复当base64字符串长度不为4的倍数时,解码失败的问题
update: 提高根据ISO匹配国家名称的正确率
fix: 修复vmess的port和aid不规范导致无法解析的问题
modify: 一些没用的修改
2023-09-24 18:06:44 +08:00
nite 38352d4cd7 fix: 修复当base64字符串长度不为4的倍数时,解码失败的问题
update: 提高根据ISO匹配国家名称的正确率
2023-09-24 14:48:42 +08:00
nite fc21e35465 feat: webui解析url到页面 2023-09-24 12:31:12 +08:00
nite 159a3562d4 fix: 修复必要文件夹不存在程序直接崩溃的问题 2023-09-23 15:12:48 +08:00
Nite07 34b85c8d63 v0.0.5
feat: 增加节点去重
feat: 增加节点重命名
feat: 增加节点过滤
feat: 增加短链密码
modify: 修改模板解析逻辑,现在需要添加 <all>,<countries> 来让程序解析模板
modify: 修改短链请求逻辑,不再跳转链接,而是服务器内部请求
modify: 完善 Meta 默认模板
如果你从旧版升级,请务必修改或删除程序目录下的模板
2023-09-23 09:09:09 +08:00
nite 3546396e3d fix 2023-09-23 08:48:45 +08:00
nite be9df16b61 fix 2023-09-23 01:55:43 +08:00
nite 67e4121fa6 fix 2023-09-23 01:31:04 +08:00
nite c1e9099156 fix 2023-09-23 01:11:58 +08:00
nite 429788a19f fix 2023-09-23 00:58:57 +08:00
nite 2339b7d256 feat: 增加重复节点检测
feat: 增加节点名称字符串替换
feat: 增加节点删除
feat: 增加短链密码设定
modify: 修改模板解析逻辑
2023-09-22 23:43:26 +08:00
nite 06c9858866 modify: 增加debug输出 2023-09-21 17:37:37 +08:00
Nite07 8d06ab3175 Dev (#2)
fix: 修复当订阅链接有多个 clash 配置时丢失节点的问题
update: 增加检测更新
modify: 修改数据库路径
modify: 修改短链生成逻辑
modify: 统一输出信息
2023-09-21 09:08:02 +08:00
nite f166c6a54a fix 2023-09-17 17:24:30 +08:00
nite c0e6b62625 fix 2023-09-17 17:02:59 +08:00
nite 354379b12a fix 2023-09-17 16:59:02 +08:00
nite 4f4a633035 fix 2023-09-17 16:20:15 +08:00
nite be9bdd269e update: 使用glebarez/sqlite替代gorm.io/driver/sqlite实现无需CGO运行 2023-09-17 16:14:31 +08:00
nite 6b08b2cb86 feat: 增加短链生成 2023-09-17 15:52:37 +08:00
nite 38dbea4a2a fix: 修复只提供节点链接时空指针的问题
fix: 修复ssr分享链接加密时无法解析的问题
fix: 修复ssr解析缺少Name属性的问题
fix: 修复ssr解析时没有解码参数的问题
2023-09-17 13:21:01 +08:00
nite 1788541e04 fix 2023-09-17 10:46:49 +08:00
nite d38d5bcb70 feat: Web UI
update: 校验Rule-Provider是否有重名
modify: 修改meta默认模板
modify: 根据Clash筛选返回配置中的节点类型
2023-09-17 10:36:40 +08:00
nite 918521682c modify: 根据Clash筛选返回配置中的节点类型 2023-09-16 21:46:59 +08:00
Nite07 3318f5f2db Update go.yml 2023-09-15 10:47:23 +08:00
Nite07 300bb4931d Merge pull request #1 from nitezs/dev
Dev
2023-09-15 10:39:44 +08:00
nite 8e53183685 fix: 修复规则列表顺序错误,导致无法拦截广告的问题 2023-09-15 10:29:10 +08:00
nite 1f38125e85 update: 修改 docker.yml 镜像构建规则 2023-09-15 09:59:21 +08:00
nite 4e7d84499f fix: 修改 Dockerfile 工作目录 2023-09-15 09:15:24 +08:00
nite 282649aca0 fix 2023-09-15 08:51:22 +08:00
nite fe2f47865e fix 2023-09-15 08:26:48 +08:00
nite f6273947e7 mod 2023-09-15 00:31:57 +08:00
nite 0f2dba21eb mod: 增加 dev 分支的 docker 镜像构建 2023-09-15 00:17:32 +08:00
nite 72635ce0fc feat: 增加国家策略组排序策略
feat: 支持指定本地模板
mod: Rule-Provider 可以自定义名称
2023-09-15 00:13:45 +08:00
Nite07 fd1ff69711 Update README.md 2023-09-14 18:02:04 +08:00
nite 52e609298a add:goreleaser.yaml
mod:docker.yml
mod:go.yml
mod:docker-compose.yml
mod:README.md
2023-09-14 17:59:17 +08:00
nite d6539a0707 update 2023-09-13 15:52:37 +08:00
nite 070d8b5eb5 update 2023-09-13 15:44:48 +08:00
Nite07 5c71c54276 Rename docker to docker.yaml 2023-09-13 15:27:23 +08:00
Nite07 83ce5349c9 Create docker 2023-09-13 15:26:56 +08:00
Nite07 f1bdeddfba Update go.yml 2023-09-13 15:15:46 +08:00
nite 1866e6bf67 update 2023-09-13 15:06:44 +08:00
109 changed files with 10675 additions and 1476 deletions
-7
View File
@@ -1,7 +0,0 @@
PORT=8011
META_TEMPLATE=meta_template.json
CLASH_TEMPLATE=clash_template.json
REQUEST_RETRY_TIMES=3
REQUEST_MAX_FILE_SIZE=1048576
CACHE_EXPIRE=300
LOG_LEVEL=info
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+57
View File
@@ -0,0 +1,57 @@
name: docker
on:
push:
tags:
- "v*"
jobs:
prepare-and-build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
nite07/sub2clash
ghcr.io/bestnite/sub2clash
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=match,pattern=(alpha|beta|rc),group=1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
build-args: |
"version=${{ github.ref_name }}"
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
file: Containerfile
-120
View File
@@ -1,120 +0,0 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build
run: |
LDFLAGS="-s -w"
# Linux
GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-amd64 main.go
GOOS=linux GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm main.go
GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm64 main.go
# Darwin
GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-amd64 main.go
GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-arm64 main.go # For Apple Silicon
# Windows
GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-amd64.exe main.go
GOOS=windows GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm.exe main.go
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload Release Asset (Linux amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-amd64
asset_name: sub2clash-linux-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux arm)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm
asset_name: sub2clash-linux-arm
asset_content_type: application/octet-stream
- name: Upload Release Asset (Linux arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-linux-arm64
asset_name: sub2clash-linux-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-amd64
asset_name: sub2clash-darwin-amd64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Darwin arm64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-darwin-arm64
asset_name: sub2clash-darwin-arm64
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows amd64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-amd64.exe
asset_name: sub2clash-windows-amd64.exe
asset_content_type: application/octet-stream
- name: Upload Release Asset (Windows arm)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./output/sub2clash-windows-arm.exe
asset_name: sub2clash-windows-arm.exe
asset_content_type: application/octet-stream
+38
View File
@@ -0,0 +1,38 @@
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
- name: Install UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: setup node
uses: actions/setup-node@v6
with:
node-version: "latest"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -1
View File
@@ -1,5 +1,10 @@
.idea .idea
dist dist
subs subs
test
logs logs
data
.env
config.yaml
config.yml
config.json
.codex
+47
View File
@@ -0,0 +1,47 @@
version: 2
project_name: sub2clash
before:
hooks:
- bash ./build-frontend.sh {{ .Version }}
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/bestnite/sub2clash/constant.Version={{ .Version }}
flags:
- -trimpath
archives:
- formats: ["tar.gz"]
format_overrides:
- formats: ["zip"]
goos: windows
wrap_in_directory: true
files:
- LICENSE
- README.md
- templates
release:
draft: true
# upx:
# - enabled: true
# compress: best
nfpms:
- id: sub2clash
homepage: https://github.com/bestnite/sub2clash
maintainer: Nite <admin@nite07.com>
license: "MIT"
formats:
- apk
- deb
- rpm
- termux.deb
- archlinux
provides:
- sub2clash
+14
View File
@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "debug",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"args": [],
"preLaunchTask": "build frontend"
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build frontend",
"type": "shell",
"command": "npm",
"args": [
"run",
"build"
],
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}/server/frontend"
}
}
]
}
+21
View File
@@ -0,0 +1,21 @@
FROM node:latest AS frontend_builder
WORKDIR /app/server/frontend
COPY server/frontend/package*.json ./
RUN npm install
COPY server/frontend .
ARG version
ENV VITE_APP_VERSION=${version}
RUN npm run build
FROM golang:1.25 AS builder
WORKDIR /app
COPY . .
COPY --from=frontend_builder /app/server/frontend/dist /app/server/frontend/dist
RUN go mod download
ARG version
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X github.com/bestnite/sub2clash/constant.Version=${version}" -o sub2clash .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/sub2clash /app/sub2clash
ENTRYPOINT ["/app/sub2clash"]
View File
+111 -25
View File
@@ -1,47 +1,133 @@
# sub2clash # sub2clash
将订阅链接转换为 Clash、Clash.Meta 配置 将订阅链接转换为 Clash、Clash.Meta 配置
[预览](https://clash.nite07.com/)
## 特性 ## 特性
- 开箱即用的规则、策略组配置 - 开箱即用的规则、策略组配置
- 自动根据节点名称按国家划分策略组 - 自动根据节点名称按国家划分策略组
- 支持多订阅合并 - 多订阅合并
- 自定义 Rule Provider、Rule
- 支持多种协议 - 支持多种协议
- [x] Shadowsocks - Shadowsocks
- [x] ShadowsocksR - ShadowsocksR
- [x] Vmess - Vmess
- [x] Vless - Vless Clash.Meta
- [x] Trojan - Trojan
- [ ] Hysteria - Hysteria Clash.Meta
- [ ] TUIC - Hysteria2 Clash.Meta
- [ ] WireGuard - Socks5
- Anytls Clash.Meta
## API ## 使用
### `/clash`,`/meta` ### 部署
- [docker compose](./compose.yml)
- 运行[二进制文件](https://github.com/bestnite/sub2clash/releases/latest)
### 配置
支持多种配置方式,按优先级排序:
1. **配置文件**:支持多种格式(YAML、JSON),按以下优先级搜索:
- `config.yaml` / `config.yml`
- `config.json`
- `sub2clash.yaml` / `sub2clash.yml`
- `sub2clash.json`
2. **环境变量**:使用 `SUB2CLASH_` 前缀,例如 `SUB2CLASH_ADDRESS=0.0.0.0:8011`
3. **默认值**:内置默认配置
| 配置项 | 环境变量 | 说明 | 默认值 |
| --------------------- | ------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| address | SUB2CLASH_ADDRESS | 服务监听地址 | `0.0.0.0:8011` |
| meta_template | SUB2CLASH_META_TEMPLATE | 默认 meta 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml` |
| clash_template | SUB2CLASH_CLASH_TEMPLATE | 默认 clash 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml` |
| request_retry_times | SUB2CLASH_REQUEST_RETRY_TIMES | 请求重试次数 | `3` |
| request_max_file_size | SUB2CLASH_REQUEST_MAX_FILE_SIZE | 请求文件最大大小(byte) | `1048576` |
| cache_expire | SUB2CLASH_CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
| log_level | SUB2CLASH_LOG_LEVEL | 日志等级:`debug`,`info`,`warn`,`error` | `info` |
| short_link_length | SUB2CLASH_SHORT_LINK_LENGTH | 短链长度 | `6` |
#### 配置文件示例
参考示例文件:
- [config.example.yaml](./config.example.yaml) - YAML 格式
- [config.example.json](./config.example.json) - JSON 格式
### API
#### `GET /convert/:config`
获取 Clash/Clash.Meta 配置链接 获取 Clash/Clash.Meta 配置链接
| Query 参数 | 类型 | 是否必须 | 说明 | | Path 参数 | 类型 | 说明 |
|--------------|--------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| | --------- | ------ | ---------------------------------------------- |
| sub | string | sub/proxy 至少有一项存在 | 订阅链接(可以输入多个,用 `,` 分隔) | | config | string | Base64 URL Safe 编码后的 JSON 字符串,格式如下 |
| proxy | string | sub/proxy 至少有一项存在 | 节点分享链接(可以输入多个,用 `,` 分隔) |
| refresh | bool | 否(默认 `false`) | 强制刷新配置(默认缓存 5 分钟) |
| template | string | 否 | 外部模板 |
| ruleProvider | string | 否 | 格式 `[Behavior,Url,Group,Prepend],[Behavior,Url,Group,Prepend],...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
| rule | string | 否 | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) |
| autoTest | bool | 否(默认 `false`) | 指定国家策略组是否自动测速 |
| lazy | bool | 否(默认 `false`) | 自动测速是否启用 lazy |
## 默认模板 ##### `config` JSON 结构
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
| ------------------ | ----------------- | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------- |
| clashType | int | 是 | 1 | 配置文件类型 (1: Clash, 2: Clash.Meta) |
| subscriptions | []string | sub/proxy 至少有一项存在 | - | 订阅链接(v2ray 或 clash 格式),可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个) |
| proxies | []string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个) |
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
| template | string | 否 | - | 外部模板链接或内部模板名称 |
| ruleProviders | []RuleProvider | 否 | - | 规则 |
| rules | []Rule | 否 | - | 规则 |
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc``namedesc``sizeasc``sizedesc` |
| replace | map[string]string | 否 | - | 通过正则表达式重命名节点 |
| remove | string | 否 | - | 通过正则表达式删除节点 |
| nodeList | bool | 否 | `false` | 只输出节点 |
| ignoreCountryGroup | bool | 否 | `false` | 是否忽略国家分组 |
| userAgent | string | 否 | - | 订阅 user-agent |
| useUDP | bool | 否 | `false` | 是否使用 UDP |
###### `RuleProvider` 结构
| 字段 | 类型 | 说明 |
| -------- | ------ | ---------------------------------------------------------------- |
| behavior | string | rule-set 的 behavior |
| url | string | rule-set 的 url |
| group | string | 该规则集使用的策略组名 |
| prepend | bool | 如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部 |
| name | string | 该 rule-provider 的名称,不能重复 |
###### `Rule` 结构
| 字段 | 类型 | 说明 |
| ------- | ------ | ---------------------------------------------------------------- |
| rule | string | 规则 |
| prepend | bool | 如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部 |
### 模板
可以通过变量自定义模板中的策略组代理节点
具体参考下方默认模板
- `<all>` 为添加所有节点
- `<countries>` 为添加所有国家策略组
- `<地区二位字母代码>` 为添加指定地区所有节点,例如 `<hk>` 将添加所有香港节点
#### 默认模板
- [Clash](./templates/template_clash.yaml) - [Clash](./templates/template_clash.yaml)
- [Clash.Meta](./templates/template_meta.yaml) - [Clash.Meta](./templates/template_meta.yaml)
## 已知问题 ## 开发
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue ### 添加新协议支持
## TODO 添加新协议支持需要实现以下组件:
1.`parser` 目录下实现协议解析器,用于解析节点链接
2.`model/proxy` 目录下定义协议结构体
## 贡献者
[![](https://contrib.rocks/image?repo=bestnite/sub2clash)](https://github.com/bestnite/sub2clash/graphs/contributors)
-30
View File
@@ -1,30 +0,0 @@
package controller
import (
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
"net/http"
"sub2clash/config"
"sub2clash/validator"
)
func SubmodHandler(c *gin.Context) {
// 从请求中获取参数
query, err := validator.ParseQuery(c)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
sub, err := BuildSub(query, config.Default.ClashTemplate)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
// 输出
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}
-100
View File
@@ -1,100 +0,0 @@
package controller
import (
"crypto/md5"
"encoding/hex"
"errors"
"gopkg.in/yaml.v3"
"regexp"
"strings"
"sub2clash/model"
"sub2clash/parser"
"sub2clash/utils"
"sub2clash/validator"
)
func BuildSub(query validator.SubQuery, template string) (
*model.Subscription, error,
) {
// 定义变量
var externalTemplate = query.Template != ""
var temp *model.Subscription
var sub *model.Subscription
var err error
var templateBytes []byte
// 加载模板
if !externalTemplate {
templateBytes, err = utils.LoadTemplate(template)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
}
} else {
templateBytes, err = utils.LoadSubscription(template, query.Refresh)
if err != nil {
return nil, errors.New("加载模板失败: " + err.Error())
}
}
// 解析模板
err = yaml.Unmarshal(templateBytes, &temp)
if err != nil {
return nil, errors.New("解析模板失败: " + err.Error())
}
// 加载订阅
for i := range query.Subs {
data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
if err != nil {
return nil, errors.New("加载订阅失败: " + err.Error())
}
// 解析订阅
var proxyList []model.Proxy
err = yaml.Unmarshal(data, &sub)
if err != nil {
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://")
if reg.Match(data) {
proxyList = utils.ParseProxy(strings.Split(string(data), "\n")...)
} else {
// 如果无法直接解析,尝试Base64解码
base64, err := parser.DecodeBase64(string(data))
if err != nil {
return nil, errors.New("加载订阅失败: " + err.Error())
}
proxyList = utils.ParseProxy(strings.Split(base64, "\n")...)
}
} else {
proxyList = sub.Proxies
}
utils.AddProxy(temp, query.AutoTest, query.Lazy, proxyList...)
}
// 处理自定义代理
utils.AddProxy(temp, query.AutoTest, query.Lazy, utils.ParseProxy(query.Proxies...)...)
// 处理自定义规则
for _, v := range query.Rules {
if v.Prepend {
utils.PrependRules(temp, v.Rule)
} else {
utils.AppendRules(temp, v.Rule)
}
}
// 处理自定义 ruleProvider
for _, v := range query.RuleProviders {
hash := md5.Sum([]byte(v.Url))
name := hex.EncodeToString(hash[:])
provider := model.RuleProvider{
Type: "http",
Behavior: v.Behavior,
Url: v.Url,
Path: "./" + name + ".yaml",
Interval: 3600,
}
if v.Prepend {
utils.PrependRuleProvider(
temp, name, v.Group, provider,
)
} else {
utils.AppenddRuleProvider(
temp, name, v.Group, provider,
)
}
}
return temp, nil
}
-31
View File
@@ -1,31 +0,0 @@
package controller
import (
_ "embed"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
"net/http"
"sub2clash/config"
"sub2clash/validator"
)
func SubHandler(c *gin.Context) {
// 从请求中获取参数
query, err := validator.ParseQuery(c)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
sub, err := BuildSub(query, config.Default.MetaTemplate)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
// 输出
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}
-21
View File
@@ -1,21 +0,0 @@
package api
import (
"github.com/gin-gonic/gin"
"sub2clash/api/controller"
"sub2clash/middleware"
)
func SetRoute(r *gin.Engine) {
r.Use(middleware.ZapLogger())
r.GET(
"/clash", func(c *gin.Context) {
controller.SubmodHandler(c)
},
)
r.GET(
"/meta", func(c *gin.Context) {
controller.SubHandler(c)
},
)
}
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
export VITE_APP_VERSION=$1
cd server/frontend
npm install
npm run build
+77
View File
@@ -0,0 +1,77 @@
package database
import (
"context"
"errors"
"os"
"path/filepath"
"time"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/model"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type Database struct {
db *gorm.DB
}
func ConnectDB() (*Database, error) {
path := filepath.Join("data", "sub2clash.db")
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return nil, err
}
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
Logger: logger.Discard,
})
if err != nil {
return nil, common.NewDatabaseConnectError(err)
}
if err = db.AutoMigrate(&model.ShortLink{}); err != nil {
return nil, err
}
return &Database{
db: db,
}, nil
}
func (d *Database) FindShortLinkByID(id string) (model.ShortLink, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return gorm.G[model.ShortLink](d.db).Where("id = ?", id).First(ctx)
}
func (d *Database) CreateShortLink(shortLink *model.ShortLink) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return gorm.G[model.ShortLink](d.db).Create(ctx, shortLink)
}
func (d *Database) UpdataShortLink(id string, name string, value any) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err := gorm.G[model.ShortLink](d.db).Where("id = ?", id).Update(ctx, name, value)
return err
}
func (d *Database) CheckShortLinkIDExists(id string) (bool, error) {
_, err := d.FindShortLinkByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err
}
return true, nil
}
func (d *Database) DeleteShortLink(id string) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err := gorm.G[model.ShortLink](d.db).Where("id = ?", id).Delete(ctx)
return err
}
+192
View File
@@ -0,0 +1,192 @@
package common
import (
"errors"
"fmt"
)
// CommonError represents a structured error type for the common package
type CommonError struct {
Code ErrorCode
Message string
Cause error
}
// ErrorCode represents different types of errors
type ErrorCode string
const (
// Directory operation errors
ErrDirCreation ErrorCode = "DIRECTORY_CREATION_FAILED"
ErrDirAccess ErrorCode = "DIRECTORY_ACCESS_FAILED"
// File operation errors
ErrFileNotFound ErrorCode = "FILE_NOT_FOUND"
ErrFileRead ErrorCode = "FILE_READ_FAILED"
ErrFileWrite ErrorCode = "FILE_WRITE_FAILED"
ErrFileCreate ErrorCode = "FILE_CREATE_FAILED"
// Network operation errors
ErrNetworkRequest ErrorCode = "NETWORK_REQUEST_FAILED"
ErrNetworkResponse ErrorCode = "NETWORK_RESPONSE_FAILED"
// Template and configuration errors
ErrTemplateLoad ErrorCode = "TEMPLATE_LOAD_FAILED"
ErrTemplateParse ErrorCode = "TEMPLATE_PARSE_FAILED"
ErrConfigInvalid ErrorCode = "CONFIG_INVALID"
// Subscription errors
ErrSubscriptionLoad ErrorCode = "SUBSCRIPTION_LOAD_FAILED"
ErrSubscriptionParse ErrorCode = "SUBSCRIPTION_PARSE_FAILED"
// Regex errors
ErrRegexCompile ErrorCode = "REGEX_COMPILE_FAILED"
ErrRegexInvalid ErrorCode = "REGEX_INVALID"
// Database errors
ErrDatabaseConnect ErrorCode = "DATABASE_CONNECTION_FAILED"
ErrDatabaseQuery ErrorCode = "DATABASE_QUERY_FAILED"
ErrRecordNotFound ErrorCode = "RECORD_NOT_FOUND"
// Validation errors
ErrValidation ErrorCode = "VALIDATION_FAILED"
ErrInvalidInput ErrorCode = "INVALID_INPUT"
)
// Error returns the string representation of the error
func (e *CommonError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Unwrap returns the underlying error
func (e *CommonError) Unwrap() error {
return e.Cause
}
// Is allows error comparison
func (e *CommonError) Is(target error) bool {
if t, ok := target.(*CommonError); ok {
return e.Code == t.Code
}
return false
}
// NewError creates a new CommonError
func NewError(code ErrorCode, message string, cause error) *CommonError {
return &CommonError{
Code: code,
Message: message,
Cause: cause,
}
}
// NewSimpleError creates a new CommonError without a cause
func NewSimpleError(code ErrorCode, message string) *CommonError {
return &CommonError{
Code: code,
Message: message,
}
}
// Convenience constructors for common error types
// Directory errors
func NewDirCreationError(dirPath string, cause error) *CommonError {
return NewError(ErrDirCreation, fmt.Sprintf("failed to create directory: %s", dirPath), cause)
}
func NewDirAccessError(dirPath string, cause error) *CommonError {
return NewError(ErrDirAccess, fmt.Sprintf("failed to access directory: %s", dirPath), cause)
}
// File errors
func NewFileNotFoundError(filePath string) *CommonError {
return NewSimpleError(ErrFileNotFound, fmt.Sprintf("file not found: %s", filePath))
}
func NewFileReadError(filePath string, cause error) *CommonError {
return NewError(ErrFileRead, fmt.Sprintf("failed to read file: %s", filePath), cause)
}
func NewFileWriteError(filePath string, cause error) *CommonError {
return NewError(ErrFileWrite, fmt.Sprintf("failed to write file: %s", filePath), cause)
}
func NewFileCreateError(filePath string, cause error) *CommonError {
return NewError(ErrFileCreate, fmt.Sprintf("failed to create file: %s", filePath), cause)
}
// Network errors
func NewNetworkRequestError(url string, cause error) *CommonError {
return NewError(ErrNetworkRequest, fmt.Sprintf("network request failed for URL: %s", url), cause)
}
func NewNetworkResponseError(message string, cause error) *CommonError {
return NewError(ErrNetworkResponse, message, cause)
}
// Template errors
func NewTemplateLoadError(template string, cause error) *CommonError {
return NewError(ErrTemplateLoad, fmt.Sprintf("failed to load template: %s", template), cause)
}
func NewTemplateParseError(data []byte, cause error) *CommonError {
return NewError(ErrTemplateParse, fmt.Sprintf("failed to parse template: %s", data), cause)
}
// Subscription errors
func NewSubscriptionLoadError(url string, cause error) *CommonError {
return NewError(ErrSubscriptionLoad, fmt.Sprintf("failed to load subscription: %s", url), cause)
}
func NewSubscriptionParseError(data []byte, cause error) *CommonError {
return NewError(ErrSubscriptionParse, fmt.Sprintf("failed to parse subscription: %s", string(data)), cause)
}
// Regex errors
func NewRegexCompileError(pattern string, cause error) *CommonError {
return NewError(ErrRegexCompile, fmt.Sprintf("failed to compile regex pattern: %s", pattern), cause)
}
func NewRegexInvalidError(paramName string, cause error) *CommonError {
return NewError(ErrRegexInvalid, fmt.Sprintf("invalid regex in parameter: %s", paramName), cause)
}
// Database errors
func NewDatabaseConnectError(cause error) *CommonError {
return NewError(ErrDatabaseConnect, "failed to connect to database", cause)
}
func NewRecordNotFoundError(recordType string, id string) *CommonError {
return NewSimpleError(ErrRecordNotFound, fmt.Sprintf("%s not found: %s", recordType, id))
}
// Validation errors
func NewValidationError(field string, message string) *CommonError {
return NewSimpleError(ErrValidation, fmt.Sprintf("validation failed for %s: %s", field, message))
}
func NewInvalidInputError(paramName string, value string) *CommonError {
return NewSimpleError(ErrInvalidInput, fmt.Sprintf("invalid input for parameter %s: %s", paramName, value))
}
// IsErrorCode checks if an error has a specific error code
func IsErrorCode(err error, code ErrorCode) bool {
var commonErr *CommonError
if errors.As(err, &commonErr) {
return commonErr.Code == code
}
return false
}
// GetErrorCode extracts the error code from an error
func GetErrorCode(err error) (ErrorCode, bool) {
var commonErr *CommonError
if errors.As(err, &commonErr) {
return commonErr.Code, true
}
return "", false
}
+93
View File
@@ -0,0 +1,93 @@
package common
import (
P "github.com/bestnite/sub2clash/model/proxy"
"golang.org/x/text/collate"
"golang.org/x/text/language"
)
// proxyListDoc 只用于解析 YAML 订阅中的 proxies 字段。
// 方案 A/B 下我们不再关心订阅 YAML 里的其他 mihomo 配置项。
type proxyListDoc struct {
Proxy []P.Proxy `yaml:"proxies,omitempty"`
}
// generatedConfig 是运行期的最小叠加模型:
// 只保留本项目真正会读取、生成或修改的字段。
//
// 这里承载的是“本项目的业务叠加层”,而不是 mihomo 的完整配置模型:
// - Proxy: 解析出的节点,用于过滤、去重、分组等中间处理
// - ProxyGroup: 模板中需要参与占位符展开的组,以及本项目生成的国家组
// - Rule: 模板规则 + 用户追加规则,用于保持 MATCH 规则前插入的语义
type generatedConfig struct {
Proxy []P.Proxy `yaml:"proxies,omitempty"`
ProxyGroup []generatedGroup `yaml:"proxy-groups,omitempty"`
Rule []string `yaml:"rules,omitempty"`
}
// generatedGroup 表示本项目生成出来的代理组最小模型,
// 它不再镜像 mihomo 的完整 proxy-group 配置结构。
//
// 这里只保留“当前逻辑真正需要读写的字段”:
// - Name / Proxies:用于模板占位符展开与 patch
// - Type / Url / Interval / Tolerance / Lazy:用于输出自动测速国家组
// - Size / IsCountry:仅作为运行期辅助信息,不参与 YAML 输出
type generatedGroup struct {
Type string `yaml:"type,omitempty"`
Name string `yaml:"name,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"`
Tolerance int `yaml:"tolerance,omitempty"`
Lazy bool `yaml:"lazy"`
Size int `yaml:"-"`
IsCountry bool `yaml:"-"`
}
// generatedRulePatch 表示本项目追加/覆盖的 rule-provider 最小模型。
// 它仅用于把用户请求转换成对 templateDoc 的字段级 patch。
type generatedRulePatch struct {
Type string `yaml:"type,omitempty"`
Behavior string `yaml:"behavior,omitempty"`
Url string `yaml:"url,omitempty"`
Path string `yaml:"path,omitempty"`
Interval int `yaml:"interval,omitempty"`
Format string `yaml:"format,omitempty"`
}
type generatedGroupsSortByName []generatedGroup
type generatedGroupsSortBySize []generatedGroup
func (p generatedGroupsSortByName) Len() int {
return len(p)
}
func (p generatedGroupsSortBySize) Len() int {
return len(p)
}
func (p generatedGroupsSortByName) Less(i, j int) bool {
tags := []language.Tag{
language.English,
language.Chinese,
}
matcher := language.NewMatcher(tags)
bestMatch, _, _ := matcher.Match(language.Make("zh"))
c := collate.New(bestMatch)
return c.CompareString(p[i].Name, p[j].Name) < 0
}
func (p generatedGroupsSortBySize) Less(i, j int) bool {
if p[i].Size == p[j].Size {
return p[i].Name < p[j].Name
}
return p[i].Size < p[j].Size
}
func (p generatedGroupsSortByName) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func (p generatedGroupsSortBySize) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
+29
View File
@@ -0,0 +1,29 @@
package common
import (
"os"
)
func MKDir(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return err
}
}
return nil
}
func MkEssentialDir() error {
if err := MKDir("subs"); err != nil {
return NewDirCreationError("subs", err)
}
if err := MKDir("logs"); err != nil {
return NewDirCreationError("logs", err)
}
if err := MKDir("data"); err != nil {
return NewDirCreationError("data", err)
}
return nil
}
+96
View File
@@ -0,0 +1,96 @@
package common
import (
"strings"
"github.com/bestnite/sub2clash/model"
"github.com/bestnite/sub2clash/model/proxy"
)
func GetContryName(countryKey string) string {
countryMaps := []map[string]string{
model.CountryFlag,
model.CountryChineseName,
model.CountryISO,
model.CountryEnglishName,
}
for i, countryMap := range countryMaps {
if i == 2 {
splitChars := []string{"-", "_", " "}
key := make([]string, 0)
for _, splitChar := range splitChars {
slic := strings.Split(countryKey, splitChar)
for _, v := range slic {
if len(v) == 2 {
key = append(key, v)
}
}
}
for _, v := range key {
if country, ok := countryMap[strings.ToUpper(v)]; ok {
return country
}
}
}
for k, v := range countryMap {
if strings.Contains(countryKey, k) {
return v
}
}
}
return "其他地区"
}
func AddProxy(
sub *generatedConfig, autotest bool,
lazy bool, clashType model.ClashType, proxies ...proxy.Proxy,
) {
proxyTypes := model.GetSupportProxyTypes(clashType)
for _, proxy := range proxies {
if !proxyTypes[proxy.Type] {
continue
}
sub.Proxy = append(sub.Proxy, proxy)
haveProxyGroup := false
countryName := GetContryName(proxy.Name)
for i := range sub.ProxyGroup {
group := &sub.ProxyGroup[i]
if group.Name == countryName {
group.Proxies = append(group.Proxies, proxy.Name)
group.Size++
haveProxyGroup = true
}
}
if !haveProxyGroup {
var newGroup generatedGroup
if !autotest {
newGroup = generatedGroup{
Name: countryName,
Type: "select",
Proxies: []string{proxy.Name},
IsCountry: true,
Size: 1,
}
} else {
newGroup = generatedGroup{
Name: countryName,
Type: "url-test",
Proxies: []string{proxy.Name},
IsCountry: true,
Url: "http://www.gstatic.com/generate_204",
Interval: 300,
Tolerance: 50,
Lazy: lazy,
Size: 1,
}
}
sub.ProxyGroup = append(sub.ProxyGroup, newGroup)
}
}
}
+13
View File
@@ -0,0 +1,13 @@
package common
import "math/rand"
func RandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var result []byte
for i := 0; i < length; i++ {
result = append(result, charset[rand.Intn(len(charset))])
}
return string(result)
}
+12
View File
@@ -0,0 +1,12 @@
package common
import (
"resty.dev/v3"
)
func Request(retryTimes int) *resty.Client {
client := resty.New()
client.
SetRetryCount(retryTimes)
return client
}
+48
View File
@@ -0,0 +1,48 @@
package common
import (
"fmt"
"strings"
)
func PrependRuleProvider(
sub *generatedConfig, providerName string, group string,
) {
PrependRules(
sub,
fmt.Sprintf("RULE-SET,%s,%s", providerName, group),
)
}
func AppenddRuleProvider(
sub *generatedConfig, providerName string, group string,
) {
AppendRules(sub, fmt.Sprintf("RULE-SET,%s,%s", providerName, group))
}
// PrependRules 用于在规则头部插入新规则。
// 这通常对应用户显式要求 prepend 的场景。
func PrependRules(sub *generatedConfig, rules ...string) {
if sub.Rule == nil {
sub.Rule = make([]string, 0)
}
sub.Rule = append(rules, sub.Rule...)
}
// AppendRules 在规则尾部追加,但如果尾部已有 MATCH,则保持 MATCH 仍然是最后一条。
func AppendRules(sub *generatedConfig, rules ...string) {
if sub.Rule == nil {
sub.Rule = make([]string, 0)
}
if len(sub.Rule) == 0 {
sub.Rule = append(sub.Rule, rules...)
return
}
matchRule := sub.Rule[len(sub.Rule)-1]
if strings.Contains(matchRule, "MATCH") {
sub.Rule = append(sub.Rule[:len(sub.Rule)-1], rules...)
sub.Rule = append(sub.Rule, matchRule)
return
}
sub.Rule = append(sub.Rule, rules...)
}
+682
View File
@@ -0,0 +1,682 @@
package common
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/bestnite/sub2clash/logger"
"github.com/bestnite/sub2clash/model"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
"github.com/bestnite/sub2clash/utils"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
var subsDir = "subs"
var fileLock sync.RWMutex
func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) {
if refresh {
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
}
hash := sha256.Sum224([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
stat, err := os.Stat(fileName)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
}
lastGetTime := stat.ModTime().Unix()
if lastGetTime+cacheExpire > time.Now().Unix() {
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer func(file *os.File) {
if file != nil {
_ = file.Close()
}
}(file)
fileLock.RLock()
defer fileLock.RUnlock()
subContent, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return subContent, nil
}
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
}
func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) {
hash := sha256.Sum224([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
client := Request(retryTimes)
defer client.Close()
resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url)
if err != nil {
return nil, err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
file, err := os.Create(fileName)
if err != nil {
return nil, err
}
defer func(file *os.File) {
if file != nil {
_ = file.Close()
}
}(file)
fileLock.Lock()
defer fileLock.Unlock()
_, err = file.Write(data)
if err != nil {
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
}
return data, nil
}
// BuildSub 是当前配置转换链路的核心入口。
//
// 当前设计分为三层:
// 1. templateDoc:模板 YAML 的完整语法树,也是最终输出真源
// 2. generatedConfig:本项目运行期最小叠加层,只保存参与业务计算的字段
// 3. proxy.Proxy:节点解析后的 typed 模型,用于过滤、去重、重命名和输出
//
// 这个函数的目标不是“重建一整份 mihomo 配置”,而是:
// - 保留模板中绝大部分原始字段
// - 只对 proxies / proxy-groups / rules / rule-providers 做定点 patch
func BuildSub(clashType model.ClashType, query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) (
*BuiltSub, error,
) {
templateDoc, templateBytes, err := loadTemplateDocument(query, template, cacheExpire, retryTimes)
if err != nil {
return nil, err
}
temp, err := extractTemplateOverlay(templateDoc)
if err != nil {
logger.Logger.Debug("extract template overlay failed", zap.Error(err))
return nil, NewTemplateParseError(templateBytes, err)
}
proxyList, err := collectQueryProxies(query, cacheExpire, retryTimes)
if err != nil {
return nil, err
}
proxyList, err = normalizeProxyList(query, proxyList)
if err != nil {
return nil, err
}
// t 仅承载“由节点生成出来的新内容”,例如国家组。
// 模板里原有的组、规则等则保存在 temp 中。
generated, err := buildGeneratedConfig(clashType, query, proxyList)
if err != nil {
return nil, err
}
MergeSubAndTemplate(temp, generated, query.IgnoreCountryGrooup)
applyRulePatches(temp, query)
addedRuleProviders := buildRuleProviderPatches(query)
if err := mergeTemplateProxies(templateDoc, generated.Proxy); err != nil {
return nil, NewError(ErrConfigInvalid, "failed to update template path: proxies", err)
}
if temp.ProxyGroup == nil {
temp.ProxyGroup = make([]generatedGroup, 0)
}
if err := mergeTemplateProxyGroups(templateDoc, temp.ProxyGroup); err != nil {
return nil, NewError(ErrConfigInvalid, "failed to update template path: proxy-groups", err)
}
rulesChanged := len(query.Rules) != 0 || len(query.RuleProviders) != 0
if rulesChanged {
if temp.Rule == nil {
temp.Rule = make([]string, 0)
}
if err := SetYAMLPath(templateDoc, "rules", temp.Rule); err != nil {
return nil, NewError(ErrConfigInvalid, "failed to update template path: rules", err)
}
}
if len(query.RuleProviders) != 0 {
if err := mergeTemplateRuleProviders(templateDoc, addedRuleProviders); err != nil {
return nil, NewError(ErrConfigInvalid, "failed to update template path: rule-providers", err)
}
}
return &BuiltSub{root: templateDoc}, nil
}
// loadTemplateDocument 负责统一加载模板来源,并返回:
// 1. 解析后的 YAML 语法树
// 2. 原始模板字节,用于错误报告
func loadTemplateDocument(query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) (*yaml.Node, []byte, error) {
var err error
var templateBytes []byte
if query.Template != "" {
template = query.Template
}
if strings.HasPrefix(template, "http") {
templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, nil, NewTemplateLoadError(template, err)
}
} else {
unescape, err := url.QueryUnescape(template)
if err != nil {
return nil, nil, NewTemplateLoadError(template, err)
}
templateBytes, err = LoadTemplate(unescape)
if err != nil {
logger.Logger.Debug(
"load template failed", zap.String("template", template), zap.Error(err),
)
return nil, nil, NewTemplateLoadError(unescape, err)
}
}
templateDoc, err := ParseYAMLDocument(templateBytes)
if err != nil {
logger.Logger.Debug("parse template yaml node failed", zap.Error(err))
return nil, templateBytes, NewTemplateParseError(templateBytes, err)
}
return templateDoc, templateBytes, nil
}
// collectQueryProxies 汇总来自订阅链接和直接传入代理链接的所有节点。
func collectQueryProxies(query model.ConvertConfig, cacheExpire int64, retryTimes int) ([]P.Proxy, error) {
proxyList := make([]P.Proxy, 0)
for i := range query.Subs {
newProxies, err := loadSubscriptionProxies(query, query.Subs[i], cacheExpire, retryTimes)
if err != nil {
return nil, err
}
proxyList = append(proxyList, newProxies...)
}
if len(query.Proxies) != 0 {
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, query.Proxies...)
if err != nil {
return nil, err
}
proxyList = append(proxyList, p...)
}
return proxyList, nil
}
// loadSubscriptionProxies 负责加载单条订阅并应用订阅名作为节点前缀。
func loadSubscriptionProxies(query model.ConvertConfig, subscriptionURL string, cacheExpire int64, retryTimes int) ([]P.Proxy, error) {
data, err := LoadSubscription(subscriptionURL, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
if err != nil {
logger.Logger.Debug(
"load subscription failed", zap.String("url", subscriptionURL), zap.Error(err),
)
return nil, NewSubscriptionLoadError(subscriptionURL, err)
}
subName := ""
if strings.Contains(subscriptionURL, "#") {
subName = subscriptionURL[strings.LastIndex(subscriptionURL, "#")+1:]
}
newProxies, err := parseSubscriptionProxies(data, query.UseUDP, subscriptionURL)
if err != nil {
return nil, err
}
if subName != "" {
for i := range newProxies {
newProxies[i].SubName = subName
}
}
return newProxies, nil
}
// parseSubscriptionProxies 按“Clash YAML -> URI 列表 -> Base64 文本”的顺序容错解析节点。
func parseSubscriptionProxies(data []byte, useUDP bool, subscriptionURL string) ([]P.Proxy, error) {
sub := &proxyListDoc{}
if err := yaml.Unmarshal(data, sub); err == nil {
return sub.Proxy, nil
}
reg, err := regexp.Compile("(" + strings.Join(parser.GetAllPrefixes(), "|") + ")://")
if err != nil {
logger.Logger.Debug("compile regex failed", zap.Error(err))
return nil, NewRegexInvalidError("prefix", err)
}
if reg.Match(data) {
return parser.ParseProxies(parser.ParseConfig{UseUDP: useUDP}, strings.Split(string(data), "\n")...)
}
base64, err := utils.DecodeBase64(string(data), false)
if err != nil {
logger.Logger.Debug(
"parse subscription failed", zap.String("url", subscriptionURL),
zap.String("data", string(data)),
zap.Error(err),
)
return nil, NewSubscriptionParseError(data, err)
}
return parser.ParseProxies(parser.ParseConfig{UseUDP: useUDP}, strings.Split(base64, "\n")...)
}
// normalizeProxyList 汇总所有节点标准化步骤,确保后续分组和 patch 使用的是稳定结果。
func normalizeProxyList(query model.ConvertConfig, proxyList []P.Proxy) ([]P.Proxy, error) {
applySubscriptionPrefixes(proxyList)
var err error
proxyList, err = dedupeProxies(proxyList)
if err != nil {
return nil, err
}
proxyList, err = removeProxiesByPattern(proxyList, query.Remove)
if err != nil {
return nil, err
}
proxyList, err = replaceProxyNames(proxyList, query.Replace)
if err != nil {
return nil, err
}
ensureUniqueProxyNames(proxyList)
trimProxyNames(proxyList)
return proxyList, nil
}
func applySubscriptionPrefixes(proxyList []P.Proxy) {
for i := range proxyList {
if proxyList[i].SubName != "" {
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
}
}
}
// dedupeProxies 通过 YAML 序列化结果判定两个节点是否完全相同。
func dedupeProxies(proxyList []P.Proxy) ([]P.Proxy, error) {
proxies := make(map[string]*P.Proxy)
newProxies := make([]P.Proxy, 0, len(proxyList))
for i := range proxyList {
yamlBytes, err := yaml.Marshal(proxyList[i])
if err != nil {
logger.Logger.Debug("marshal proxy failed", zap.Error(err))
return nil, fmt.Errorf("marshal proxy failed: %w", err)
}
key := string(yamlBytes)
if _, exist := proxies[key]; !exist {
proxies[key] = &proxyList[i]
newProxies = append(newProxies, proxyList[i])
}
}
return newProxies, nil
}
func removeProxiesByPattern(proxyList []P.Proxy, pattern string) ([]P.Proxy, error) {
if strings.TrimSpace(pattern) == "" {
return proxyList, nil
}
removeReg, err := regexp.Compile(pattern)
if err != nil {
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
return nil, NewRegexInvalidError("remove", err)
}
newProxyList := make([]P.Proxy, 0, len(proxyList))
for i := range proxyList {
if removeReg.MatchString(proxyList[i].Name) {
continue
}
newProxyList = append(newProxyList, proxyList[i])
}
return newProxyList, nil
}
func replaceProxyNames(proxyList []P.Proxy, replacements map[string]string) ([]P.Proxy, error) {
if len(replacements) == 0 {
return proxyList, nil
}
for pattern, replacement := range replacements {
replaceReg, err := regexp.Compile(pattern)
if err != nil {
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
return nil, NewRegexInvalidError("replace", err)
}
for i := range proxyList {
if replaceReg.MatchString(proxyList[i].Name) {
proxyList[i].Name = replaceReg.ReplaceAllString(proxyList[i].Name, replacement)
}
}
}
return proxyList, nil
}
func ensureUniqueProxyNames(proxyList []P.Proxy) {
names := make(map[string]int)
for i := range proxyList {
if _, exist := names[proxyList[i].Name]; exist {
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
} else {
names[proxyList[i].Name] = 0
}
}
}
func trimProxyNames(proxyList []P.Proxy) {
for i := range proxyList {
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
}
}
// buildGeneratedConfig 只生成“新增内容”,例如国家组和最终可输出的节点集合。
func buildGeneratedConfig(clashType model.ClashType, query model.ConvertConfig, proxyList []P.Proxy) (*generatedConfig, error) {
generated := &generatedConfig{}
AddProxy(generated, query.AutoTest, query.Lazy, clashType, proxyList...)
sortGeneratedGroups(generated, query.Sort)
return generated, nil
}
func sortGeneratedGroups(generated *generatedConfig, sortMode string) {
switch sortMode {
case "sizeasc":
sort.Sort(generatedGroupsSortBySize(generated.ProxyGroup))
case "sizedesc":
sort.Sort(sort.Reverse(generatedGroupsSortBySize(generated.ProxyGroup)))
case "nameasc":
sort.Sort(generatedGroupsSortByName(generated.ProxyGroup))
case "namedesc":
sort.Sort(sort.Reverse(generatedGroupsSortByName(generated.ProxyGroup)))
default:
sort.Sort(generatedGroupsSortByName(generated.ProxyGroup))
}
}
// applyRulePatches 只修改运行期 overlay 中的 rules 切片,不直接写 YAML。
func applyRulePatches(temp *generatedConfig, query model.ConvertConfig) {
for _, v := range query.Rules {
if v.Prepend {
PrependRules(temp, v.Rule)
} else {
AppendRules(temp, v.Rule)
}
}
for _, v := range query.RuleProviders {
if v.Prepend {
PrependRuleProvider(temp, v.Name, v.Group)
} else {
AppenddRuleProvider(temp, v.Name, v.Group)
}
}
}
// buildRuleProviderPatches 把 API 请求中的 rule-provider 参数转换成 YAML patch payload。
func buildRuleProviderPatches(query model.ConvertConfig) map[string]generatedRulePatch {
if len(query.RuleProviders) == 0 {
return nil
}
patches := make(map[string]generatedRulePatch, len(query.RuleProviders))
for _, v := range query.RuleProviders {
hash := sha256.Sum224([]byte(v.Url))
name := hex.EncodeToString(hash[:])
patches[v.Name] = generatedRulePatch{
Type: "http",
Behavior: v.Behavior,
Url: v.Url,
Path: "./" + name + ".yaml",
Interval: 3600,
}
}
return patches
}
// extractTemplateOverlay 只从模板 YAML 树中提取本项目真正会参与计算的局部字段。
// 这让模板读取完全基于 yaml.Node,而不再依赖任何整份配置的 typed unmarshal。
func extractTemplateOverlay(templateDoc *yaml.Node) (*generatedConfig, error) {
overlay := &generatedConfig{}
if err := decodeOptionalYAMLPath(templateDoc, "proxy-groups", &overlay.ProxyGroup); err != nil {
return nil, err
}
if err := decodeOptionalYAMLPath(templateDoc, "rules", &overlay.Rule); err != nil {
return nil, err
}
return overlay, nil
}
// decodeOptionalYAMLPath 在路径存在且非 null 时才执行 Decode
// 路径不存在时保持目标值为零值。
func decodeOptionalYAMLPath(doc *yaml.Node, path string, target any) error {
node, err := GetYAMLPath(doc, path)
if err != nil {
return err
}
if node == nil || isNullYAMLNode(node) {
return nil
}
if err := node.Decode(target); err != nil {
return fmt.Errorf("decode template path %q failed: %w", path, err)
}
return nil
}
// mergeTemplateProxies 只负责把本项目生成出的代理追加到模板现有 proxies 后面。
// 模板中已有代理节点原样保留,不做 struct round-trip。
func mergeTemplateProxies(templateDoc *yaml.Node, generated []P.Proxy) error {
if len(generated) == 0 && !HasYAMLPath(templateDoc, "proxies") {
return nil
}
proxiesNode, err := EnsureYAMLSequencePath(templateDoc, "proxies")
if err != nil {
return err
}
for _, proxy := range generated {
if err := AppendYAMLSequenceValue(proxiesNode, proxy); err != nil {
return err
}
}
return nil
}
// mergeTemplateProxyGroups 负责两类更新:
// 1. 对模板中同名组,仅覆盖 proxies 字段,保留其他字段
// 2. 追加本项目新生成的国家组
func mergeTemplateProxyGroups(templateDoc *yaml.Node, groups []generatedGroup) error {
if len(groups) == 0 && !HasYAMLPath(templateDoc, "proxy-groups") {
return nil
}
groupNodes, err := EnsureYAMLSequencePath(templateDoc, "proxy-groups")
if err != nil {
return err
}
for _, group := range groups {
if group.IsCountry {
if existing := FindYAMLSequenceMappingByStringField(groupNodes, "name", group.Name); existing != nil {
continue
}
if err := AppendYAMLSequenceValue(groupNodes, group); err != nil {
return err
}
continue
}
existing := FindYAMLSequenceMappingByStringField(groupNodes, "name", group.Name)
if existing == nil {
if err := AppendYAMLSequenceValue(groupNodes, group); err != nil {
return err
}
continue
}
if findMappingValue(existing, "proxies") == nil {
continue
}
if err := SetYAMLMappingField(existing, "proxies", group.Proxies); err != nil {
return err
}
}
return nil
}
// mergeTemplateRuleProviders 以字段级 patch 的方式更新/插入 rule-provider
// 以避免覆盖模板中已有 provider 的未知字段。
func mergeTemplateRuleProviders(templateDoc *yaml.Node, providers map[string]generatedRulePatch) error {
if len(providers) == 0 && !HasYAMLPath(templateDoc, "rule-providers") {
return nil
}
providerNodes, err := EnsureYAMLMappingPath(templateDoc, "rule-providers")
if err != nil {
return err
}
for name, provider := range providers {
existing := findMappingValue(providerNodes, name)
if existing != nil && existing.Kind == yaml.MappingNode {
if err := SetYAMLMappingField(existing, "type", provider.Type); err != nil {
return err
}
if err := SetYAMLMappingField(existing, "behavior", provider.Behavior); err != nil {
return err
}
if err := SetYAMLMappingField(existing, "url", provider.Url); err != nil {
return err
}
if err := SetYAMLMappingField(existing, "path", provider.Path); err != nil {
return err
}
if err := SetYAMLMappingField(existing, "interval", provider.Interval); err != nil {
return err
}
if provider.Format != "" {
if err := SetYAMLMappingField(existing, "format", provider.Format); err != nil {
return err
}
}
continue
}
if err := SetYAMLMappingField(providerNodes, name, provider); err != nil {
return err
}
}
return nil
}
func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) {
client := Request(retryTimes)
defer client.Close()
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
if err != nil {
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
return "", NewNetworkRequestError(url, err)
}
defer resp.Body.Close()
if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
return userInfo, nil
}
logger.Logger.Debug("subscription-userinfo header not found in response")
return "", NewNetworkResponseError("subscription-userinfo header not found", nil)
}
// MergeSubAndTemplate 把“模板侧需要参与计算的最小叠加层”和“本项目生成结果”合并。
// 它只处理本项目关心的运行期结构,不负责最终 YAML 输出。
func MergeSubAndTemplate(temp *generatedConfig, sub *generatedConfig, igcg bool) {
var countryGroupNames []string
for _, proxyGroup := range sub.ProxyGroup {
if proxyGroup.IsCountry {
countryGroupNames = append(
countryGroupNames, proxyGroup.Name,
)
}
}
var proxyNames []string
for _, proxy := range sub.Proxy {
proxyNames = append(proxyNames, proxy.Name)
}
for i := range temp.ProxyGroup {
if temp.ProxyGroup[i].IsCountry {
continue
}
newProxies := make([]string, 0)
countryGroupMap := make(map[string]generatedGroup)
for _, v := range sub.ProxyGroup {
if v.IsCountry {
countryGroupMap[v.Name] = v
}
}
for j := range temp.ProxyGroup[i].Proxies {
reg := regexp.MustCompile("<(.*?)>")
if reg.Match([]byte(temp.ProxyGroup[i].Proxies[j])) {
key := reg.FindStringSubmatch(temp.ProxyGroup[i].Proxies[j])[1]
switch key {
case "all":
newProxies = append(newProxies, proxyNames...)
case "countries":
if !igcg {
newProxies = append(newProxies, countryGroupNames...)
}
default:
if !igcg {
if len(key) == 2 {
newProxies = append(
newProxies, countryGroupMap[GetContryName(key)].Proxies...,
)
}
}
}
} else {
newProxies = append(newProxies, temp.ProxyGroup[i].Proxies[j])
}
}
temp.ProxyGroup[i].Proxies = newProxies
}
if !igcg {
temp.ProxyGroup = append(temp.ProxyGroup, sub.ProxyGroup...)
}
}
+478
View File
@@ -0,0 +1,478 @@
package common
import (
"os"
"path/filepath"
"testing"
"github.com/bestnite/sub2clash/model"
"gopkg.in/yaml.v3"
)
func withRepoRoot(t *testing.T) {
t.Helper()
originalWD, err := os.Getwd()
if err != nil {
t.Fatalf("get working directory: %v", err)
}
repoRoot := filepath.Dir(originalWD)
if err := os.Chdir(repoRoot); err != nil {
t.Fatalf("change working directory: %v", err)
}
t.Cleanup(func() {
_ = os.Chdir(originalWD)
})
}
func TestBuildSubPreservesUnmodeledTemplateSections(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `mixed-port: 7890
dns:
enable: true
future-field: true
new-section:
enabled: true
proxies:
proxy-groups:
- name: 节点选择
type: select
proxies:
- <countries>
- DIRECT
rules:
- MATCH,节点选择
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#Test Node",
},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := yaml.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
dns, ok := doc["dns"].(map[string]any)
if !ok {
t.Fatalf("dns section missing: %s", output)
}
if dns["future-field"] != true {
t.Fatalf("dns future-field not preserved: %#v", dns)
}
newSection, ok := doc["new-section"].(map[string]any)
if !ok {
t.Fatalf("new-section missing: %s", output)
}
if newSection["enabled"] != true {
t.Fatalf("new-section not preserved: %#v", newSection)
}
proxies, ok := doc["proxies"].([]any)
if !ok || len(proxies) != 1 {
t.Fatalf("expected generated proxies in output: %#v", doc["proxies"])
}
rules, ok := doc["rules"].([]any)
if !ok || len(rules) != 1 || rules[0] != "MATCH,节点选择" {
t.Fatalf("rules should stay untouched without rule patches: %#v", doc["rules"])
}
}
func TestBuildSubPreservesTemplateProxyAndGroupFields(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_group_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `proxies:
- name: Template Proxy
type: ss
server: 1.1.1.1
port: 443
cipher: aes-256-gcm
password: password
future-proxy-field: keep
proxy-groups:
- name: 节点选择
type: select
future-group-field: keep
proxies:
- <countries>
- DIRECT
rules:
- MATCH,节点选择
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#Test Node",
},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := yaml.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
proxies, ok := doc["proxies"].([]any)
if !ok || len(proxies) != 2 {
t.Fatalf("expected two proxies in output: %#v", doc["proxies"])
}
firstProxy, ok := proxies[0].(map[string]any)
if !ok {
t.Fatalf("template proxy should remain a mapping: %#v", proxies[0])
}
if firstProxy["future-proxy-field"] != "keep" {
t.Fatalf("template proxy field not preserved: %#v", firstProxy)
}
groups, ok := doc["proxy-groups"].([]any)
if !ok || len(groups) == 0 {
t.Fatalf("expected proxy groups in output: %#v", doc["proxy-groups"])
}
firstGroup, ok := groups[0].(map[string]any)
if !ok {
t.Fatalf("template group should remain a mapping: %#v", groups[0])
}
if firstGroup["future-group-field"] != "keep" {
t.Fatalf("template proxy-group field not preserved: %#v", firstGroup)
}
groupProxies, ok := firstGroup["proxies"].([]any)
if !ok || len(groupProxies) == 0 {
t.Fatalf("template proxy-group proxies missing: %#v", firstGroup["proxies"])
}
for _, value := range groupProxies {
if value == "<countries>" {
t.Fatalf("placeholder should be resolved in template proxy-group: %#v", groupProxies)
}
}
}
func TestBuildSubAddsRulesForRuleProviderWhenTemplateHasNoRules(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_rule_provider_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `proxy-groups:
- name: 节点选择
type: select
proxies:
- DIRECT
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#Test Node",
},
RuleProviders: []model.RuleProviderStruct{{
Name: "test-provider",
Group: "节点选择",
Behavior: "domain",
Url: "https://example.com/rules.yaml",
}},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := yaml.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
ruleProviders, ok := doc["rule-providers"].(map[string]any)
if !ok {
t.Fatalf("rule-providers missing: %#v", doc["rule-providers"])
}
if _, ok := ruleProviders["test-provider"]; !ok {
t.Fatalf("test-provider missing: %#v", ruleProviders)
}
rules, ok := doc["rules"].([]any)
if !ok || len(rules) != 1 || rules[0] != "RULE-SET,test-provider,节点选择" {
t.Fatalf("expected generated rule for provider: %#v", doc["rules"])
}
}
func TestBuildSubDoesNotInjectProxiesFieldIntoUseBasedGroup(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_use_group_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `proxy-groups:
- name: 节点选择
type: select
use:
- provider-a
rules:
- MATCH,节点选择
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#Test Node",
},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := yaml.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
groups := doc["proxy-groups"].([]any)
firstGroup := groups[0].(map[string]any)
if _, exists := firstGroup["proxies"]; exists {
t.Fatalf("use-based group should not gain proxies field: %#v", firstGroup)
}
if _, exists := firstGroup["use"]; !exists {
t.Fatalf("use-based group should preserve use field: %#v", firstGroup)
}
}
func TestBuildSubPreservesUnknownFieldsOnExistingRuleProvider(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_existing_provider_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `proxy-groups:
- name: 节点选择
type: select
proxies:
- DIRECT
rule-providers:
test-provider:
type: http
behavior: classical
url: https://old.example.com/rules.yaml
path: ./old.yaml
interval: 10
future-provider-field: keep
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#Test Node",
},
RuleProviders: []model.RuleProviderStruct{{
Name: "test-provider",
Group: "节点选择",
Behavior: "domain",
Url: "https://example.com/rules.yaml",
}},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := yaml.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
ruleProviders := doc["rule-providers"].(map[string]any)
provider := ruleProviders["test-provider"].(map[string]any)
if provider["future-provider-field"] != "keep" {
t.Fatalf("existing provider field not preserved: %#v", provider)
}
if provider["behavior"] != "domain" {
t.Fatalf("provider behavior not updated: %#v", provider)
}
if provider["url"] != "https://example.com/rules.yaml" {
t.Fatalf("provider url not updated: %#v", provider)
}
}
func TestBuildSubSkipsDuplicateCountryGroupNames(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_country_group_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `proxy-groups:
- name: 其他地区
type: select
proxies:
- DIRECT
rules:
- MATCH,其他地区
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#UnknownCountryNode",
},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := yaml.Marshal(result)
if err != nil {
t.Fatalf("marshal result: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
groups := doc["proxy-groups"].([]any)
count := 0
for _, item := range groups {
group := item.(map[string]any)
if group["name"] == "其他地区" {
count++
}
}
if count != 1 {
t.Fatalf("expected duplicate country group names to be skipped, got %d entries: %#v", count, groups)
}
}
func TestBuiltSubMarshalNodeListYAMLUsesFinalYAMLTree(t *testing.T) {
withRepoRoot(t)
templateName := "test_scheme_a_nodelist_template.yaml"
templatePath := filepath.Join(templatesDir, templateName)
templateContent := `proxies:
- name: Template Proxy
type: ss
server: 1.1.1.1
port: 443
cipher: aes-256-gcm
password: password
future-proxy-field: keep
proxy-groups:
- name: 节点选择
type: select
proxies:
- DIRECT
`
if err := os.WriteFile(templatePath, []byte(templateContent), 0o644); err != nil {
t.Fatalf("write template: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(templatePath)
})
result, err := BuildSub(model.Clash, model.ConvertConfig{
ClashType: model.Clash,
Proxies: []string{
"ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080#Generated Node",
},
}, templateName, 0, 0)
if err != nil {
t.Fatalf("build subscription: %v", err)
}
output, err := result.MarshalNodeListYAML()
if err != nil {
t.Fatalf("marshal node list: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(output, &doc); err != nil {
t.Fatalf("unmarshal output: %v", err)
}
proxies, ok := doc["proxies"].([]any)
if !ok || len(proxies) != 2 {
t.Fatalf("expected node list to include template and generated proxies: %#v", doc["proxies"])
}
firstProxy, ok := proxies[0].(map[string]any)
if !ok {
t.Fatalf("template proxy should remain a mapping: %#v", proxies[0])
}
if firstProxy["future-proxy-field"] != "keep" {
t.Fatalf("node list should be built from final yaml tree: %#v", firstProxy)
}
}
+42
View File
@@ -0,0 +1,42 @@
package common
import (
"io"
"os"
"path/filepath"
"strings"
)
const templatesDir = "templates"
// LoadTemplate 只读取运行目录下的 templates 目录,防止其他文件内容泄漏
func LoadTemplate(templateName string) ([]byte, error) {
// 清理路径,防止目录遍历攻击
cleanTemplateName := filepath.Clean(templateName)
// 检查是否尝试访问父目录
if strings.HasPrefix(cleanTemplateName, "..") || strings.Contains(cleanTemplateName, string(filepath.Separator)+".."+string(filepath.Separator)) {
return nil, NewFileNotFoundError(templateName) // 拒绝包含父目录的路径
}
// 构建完整路径,确保只从 templates 目录读取
fullPath := filepath.Join(templatesDir, cleanTemplateName)
if _, err := os.Stat(fullPath); err == nil {
file, err := os.Open(fullPath)
if err != nil {
return nil, err
}
defer func(file *os.File) {
if file != nil {
_ = file.Close()
}
}(file)
result, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return result, nil
}
return nil, NewFileNotFoundError(templateName)
}
+383
View File
@@ -0,0 +1,383 @@
package common
import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
)
// BuiltSub 保存最终输出所需的完整 YAML 树。
//
// 这里刻意不再保存整份 typed 配置副本:
// - root 是整个转换流程的最终产物
// - 所有常规输出都直接从 root 序列化
// - nodeList 模式也从 root 中提取 proxies,而不是依赖额外状态
type BuiltSub struct {
root *yaml.Node
}
// MarshalYAML 让 BuiltSub 在输出时直接复用 patch 后的 YAML 树,
// 从而避免再次经过 struct round-trip 丢失未知字段。
func (b *BuiltSub) MarshalYAML() (any, error) {
if b == nil || b.root == nil {
return nil, nil
}
if b.root.Kind == yaml.DocumentNode {
if len(b.root.Content) == 0 {
return nil, nil
}
return b.root.Content[0], nil
}
return b.root, nil
}
// MarshalNodeListYAML 从最终 YAML 树中提取 proxies 节点,构造 nodeList 模式输出。
// 这样 nodeList 也直接复用最终 root,而不是依赖额外的 typed struct 副本。
func (b *BuiltSub) MarshalNodeListYAML() ([]byte, error) {
if b == nil || b.root == nil {
return yaml.Marshal(&yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"})
}
proxiesNode, err := GetYAMLPath(b.root, "proxies")
if err != nil {
return nil, err
}
root := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
if proxiesNode != nil && !isNullYAMLNode(proxiesNode) {
setMappingValue(root, "proxies", cloneYAMLNode(proxiesNode))
}
return yaml.Marshal(root)
}
// ParseYAMLDocument 把原始 YAML 解析成 DocumentNode
// 并确保根内容最终是一个可写入的 mapping 节点。
func ParseYAMLDocument(data []byte) (*yaml.Node, error) {
var doc yaml.Node
if err := yaml.Unmarshal(data, &doc); err != nil {
return nil, err
}
if _, err := rootMappingNode(&doc); err != nil {
return nil, err
}
return &doc, nil
}
// HasYAMLPath 判断某个点路径是否存在。
// 这里仅关心“是否找到节点”,不关心节点具体类型。
func HasYAMLPath(doc *yaml.Node, path string) bool {
current, err := GetYAMLPath(doc, path)
return err == nil && current != nil
}
// GetYAMLPath 按 a.b.c 这种点路径向下查找节点。
// 当前实现只支持 mapping 之间的逐层下钻,不处理数组索引路径。
func GetYAMLPath(doc *yaml.Node, path string) (*yaml.Node, error) {
segments := splitYAMLPath(path)
if len(segments) == 0 {
return nil, fmt.Errorf("yaml path is empty")
}
current, err := rootMappingNode(doc)
if err != nil {
return nil, err
}
for _, segment := range segments {
next := findMappingValue(current, segment)
if next == nil {
return nil, nil
}
current = next
}
return current, nil
}
// SetYAMLPath 按点路径写入一个值;不存在的中间层会自动补成 mapping。
// 例如 a.b.c=1 会在缺失时依次创建 a 和 b 两层对象节点。
func SetYAMLPath(doc *yaml.Node, path string, value any) error {
segments := splitYAMLPath(path)
if len(segments) == 0 {
return fmt.Errorf("yaml path is empty")
}
current, err := rootMappingNode(doc)
if err != nil {
return err
}
for idx, segment := range segments[:len(segments)-1] {
next := findMappingValue(current, segment)
if next == nil {
next = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
setMappingValue(current, segment, next)
}
if next.Kind != yaml.MappingNode {
return fmt.Errorf("yaml path %q segment %q is not a mapping", path, strings.Join(segments[:idx+1], "."))
}
current = next
}
encoded, err := encodeYAMLNode(value)
if err != nil {
return err
}
setMappingValue(current, segments[len(segments)-1], encoded)
return nil
}
// EnsureYAMLSequencePath 确保某个路径最终是 sequence(YAML 数组)节点。
// 不存在时会自动创建,已存在但类型不匹配时返回错误。
func EnsureYAMLSequencePath(doc *yaml.Node, path string) (*yaml.Node, error) {
return ensureYAMLPathKind(doc, path, yaml.SequenceNode, "!!seq")
}
// EnsureYAMLMappingPath 确保某个路径最终是 mapping(YAML 对象)节点。
func EnsureYAMLMappingPath(doc *yaml.Node, path string) (*yaml.Node, error) {
return ensureYAMLPathKind(doc, path, yaml.MappingNode, "!!map")
}
// SetYAMLMappingField 在一个 mapping 节点里设置单个字段。
// 它等价于“在当前对象上写 key: value”。
func SetYAMLMappingField(node *yaml.Node, key string, value any) error {
if node == nil || node.Kind != yaml.MappingNode {
return fmt.Errorf("yaml node is not a mapping")
}
encoded, err := encodeYAMLNode(value)
if err != nil {
return err
}
setMappingValue(node, key, encoded)
return nil
}
// AppendYAMLSequenceValue 向 sequence 节点末尾追加一个元素。
func AppendYAMLSequenceValue(node *yaml.Node, value any) error {
if node == nil || node.Kind != yaml.SequenceNode {
return fmt.Errorf("yaml node is not a sequence")
}
encoded, err := encodeYAMLNode(value)
if err != nil {
return err
}
node.Content = append(node.Content, encoded)
return nil
}
// FindYAMLSequenceMappingByStringField 在 YAML 数组中查找一个对象元素,
// 要求该对象存在指定字段且字段值等于目标字符串。
//
// 例如在 proxy-groups 里按 name 查找:
// - name: 节点选择
// type: select
func FindYAMLSequenceMappingByStringField(node *yaml.Node, field string, value string) *yaml.Node {
if node == nil || node.Kind != yaml.SequenceNode {
return nil
}
for _, item := range node.Content {
if item == nil || item.Kind != yaml.MappingNode {
continue
}
fieldNode := findMappingValue(item, field)
if fieldNode == nil || fieldNode.Kind != yaml.ScalarNode {
continue
}
if fieldNode.Value == value {
return item
}
}
return nil
}
// splitYAMLPath 把 a.b.c 这种点路径拆成 [a b c]。
// 空片段会被忽略,避免出现连续点号时产生无意义路径段。
func splitYAMLPath(path string) []string {
parts := strings.Split(path, ".")
segments := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
segments = append(segments, part)
}
return segments
}
// ensureYAMLPathKind 是 EnsureYAMLSequencePath / EnsureYAMLMappingPath 的底层实现。
// 它会:
// 1. 逐层确保中间节点存在且都是 mapping
// 2. 确保最后一个节点存在,且类型符合预期
func ensureYAMLPathKind(doc *yaml.Node, path string, kind yaml.Kind, tag string) (*yaml.Node, error) {
segments := splitYAMLPath(path)
if len(segments) == 0 {
return nil, fmt.Errorf("yaml path is empty")
}
current, err := rootMappingNode(doc)
if err != nil {
return nil, err
}
// 跳过最后一个元素在后面处理
for idx, segment := range segments[:len(segments)-1] {
next := findMappingValue(current, segment)
if next == nil || isNullYAMLNode(next) {
next = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
setMappingValue(current, segment, next)
}
if next.Kind != yaml.MappingNode {
return nil, fmt.Errorf("yaml path %q segment %q is not a mapping", path, strings.Join(segments[:idx+1], "."))
}
current = next
}
lastSegment := segments[len(segments)-1]
node := findMappingValue(current, lastSegment)
if node == nil || isNullYAMLNode(node) {
node = &yaml.Node{Kind: kind, Tag: tag}
setMappingValue(current, lastSegment, node)
}
if node.Kind != kind {
return nil, fmt.Errorf("yaml path %q is not a %s", path, yamlKindName(kind))
}
return node, nil
}
// rootMappingNode 统一把“文档根”整理成一个可操作的 mapping 节点。
//
// yaml.v3 通常把整份 YAML 包在 DocumentNode 下,真正的内容位于 Content[0]。
// 当前项目的 patch 逻辑都假定最外层是 key-value 结构,因此这里会:
// 1. 处理空文档
// 2. 取出 DocumentNode 的实际根内容
// 3. 确保该根内容是 mapping
func rootMappingNode(doc *yaml.Node) (*yaml.Node, error) {
if doc == nil {
return nil, fmt.Errorf("yaml document is nil")
}
root := doc
if doc.Kind == 0 {
doc.Kind = yaml.DocumentNode
}
if doc.Kind == yaml.DocumentNode {
if len(doc.Content) == 0 {
doc.Content = append(doc.Content, &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"})
}
root = doc.Content[0]
}
if root.Kind == 0 {
root.Kind = yaml.MappingNode
root.Tag = "!!map"
}
if root.Kind != yaml.MappingNode {
return nil, fmt.Errorf("yaml root must be a mapping node")
}
return root, nil
}
// isNullYAMLNode 判断一个节点是否为空/未初始化/null。
// 这让我们在“路径不存在”和“路径存在但值为 null”时都能按缺失处理。
func isNullYAMLNode(node *yaml.Node) bool {
if node == nil {
return true
}
if node.Kind == 0 {
return true
}
return node.Kind == yaml.ScalarNode && node.Tag == "!!null"
}
// yamlKindName 仅用于生成更可读的错误信息。
func yamlKindName(kind yaml.Kind) string {
switch kind {
case yaml.MappingNode:
return "mapping"
case yaml.SequenceNode:
return "sequence"
case yaml.ScalarNode:
return "scalar"
case yaml.DocumentNode:
return "document"
default:
return "node"
}
}
// findMappingValue 在 mapping 节点中按 key 查找对应的 value 节点。
//
// 需要注意:yaml.v3 的 MappingNode.Content 不是 map,而是交替存储:
// [key1, value1, key2, value2, ...]
// 所以这里每次 idx += 2,依次跳过一个完整的 key-value 对。
func findMappingValue(node *yaml.Node, key string) *yaml.Node {
if node == nil || node.Kind != yaml.MappingNode {
return nil
}
for idx := 0; idx+1 < len(node.Content); idx += 2 {
if node.Content[idx].Value == key {
return node.Content[idx+1]
}
}
return nil
}
// setMappingValue 在 mapping 节点中设置 key 对应的 value。
// 如果 key 已存在,就原位替换;否则在末尾追加一组新的 key-value。
func setMappingValue(node *yaml.Node, key string, value *yaml.Node) {
for idx := 0; idx+1 < len(node.Content); idx += 2 {
if node.Content[idx].Value == key {
node.Content[idx+1] = value
return
}
}
node.Content = append(node.Content,
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key},
value,
)
}
// encodeYAMLNode 把普通 Go 值编码成 *yaml.Node,方便统一塞回 YAML 树。
// 如果 Encode 产生的是 DocumentNode,这里会自动取出它的实际内容节点。
func encodeYAMLNode(value any) (*yaml.Node, error) {
var node yaml.Node
if err := node.Encode(value); err != nil {
return nil, err
}
if node.Kind == yaml.DocumentNode {
if len(node.Content) == 0 {
return &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}, nil
}
return node.Content[0], nil
}
return &node, nil
}
// cloneYAMLNode 深拷贝一个节点树,避免把同一个子树同时挂到多个输出根下。
func cloneYAMLNode(node *yaml.Node) *yaml.Node {
if node == nil {
return nil
}
clone := *node
if len(node.Content) != 0 {
clone.Content = make([]*yaml.Node, len(node.Content))
for i := range node.Content {
clone.Content[i] = cloneYAMLNode(node.Content[i])
}
}
return &clone
}
+11
View File
@@ -0,0 +1,11 @@
name: sub2clash
services:
sub2clash:
restart: unless-stopped
image: nite07/sub2clash:latest
ports:
- "8011:8011"
volumes:
# - ./logs:/app/logs
# - ./templates:/app/templates
- ./data:/app/data
+10
View File
@@ -0,0 +1,10 @@
{
"address": "0.0.0.0:8011",
"meta_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml",
"clash_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml",
"request_retry_times": 3,
"request_max_file_size": 1048576,
"cache_expire": 300,
"log_level": "info",
"short_link_length": 6
}
+22
View File
@@ -0,0 +1,22 @@
# Sub2Clash 配置文件示例
# 复制此文件为 config.yaml 并根据需要修改配置
# 服务端口
address: "0.0.0.0:8011"
# 模板配置
meta_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml"
clash_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml"
# 请求配置
request_retry_times: 3
request_max_file_size: 1048576 # 1MB in bytes
# 缓存配置 (秒)
cache_expire: 300 # 5 minutes
# 日志级别 (debug, info, warn, error)
log_level: "info"
# 短链接长度
short_link_length: 6
+62 -58
View File
@@ -1,72 +1,76 @@
package config package config
import ( import (
"github.com/joho/godotenv" "strings"
"os"
"strconv" "github.com/spf13/viper"
) )
type Config struct { type Config struct {
Port int Address string `mapstructure:"address"`
MetaTemplate string MetaTemplate string `mapstructure:"meta_template"`
ClashTemplate string ClashTemplate string `mapstructure:"clash_template"`
RequestRetryTimes int RequestRetryTimes int `mapstructure:"request_retry_times"`
RequestMaxFileSize int64 RequestMaxFileSize int64 `mapstructure:"request_max_file_size"`
CacheExpire int64 CacheExpire int64 `mapstructure:"cache_expire"`
LogLevel string LogLevel string `mapstructure:"log_level"`
ShortLinkLength int `mapstructure:"short_link_length"`
} }
var Default *Config var GlobalConfig *Config
var Dev string
func init() { func LoadConfig() error {
Default = &Config{ v := viper.New()
MetaTemplate: "template_meta.yaml",
ClashTemplate: "template_clash.yaml", // 添加配置文件搜索路径
RequestRetryTimes: 3, v.AddConfigPath(".")
RequestMaxFileSize: 1024 * 1024 * 1, v.AddConfigPath("./config")
Port: 8011, v.AddConfigPath("/etc/sub2clash/")
CacheExpire: 60 * 5,
LogLevel: "info", // 设置默认值
} setDefaults(v)
err := godotenv.Load()
if err != nil { // 设置环境变量前缀和自动绑定
return v.SetEnvPrefix("SUB2CLASH")
} v.AutomaticEnv()
if os.Getenv("PORT") != "" { v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
atoi, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil { // 尝试按优先级加载不同格式的配置文件
panic("PORT invalid") configLoaded := false
configNames := []string{"config", "sub2clash"}
configExts := []string{"yaml", "yml", "json"}
for _, name := range configNames {
for _, ext := range configExts {
v.SetConfigName(name)
v.SetConfigType(ext)
if err := v.ReadInConfig(); err == nil {
configLoaded = true
break
}
} }
Default.Port = atoi if configLoaded {
} break
if os.Getenv("META_TEMPLATE") != "" {
Default.MetaTemplate = os.Getenv("META_TEMPLATE")
}
if os.Getenv("CLASH_TEMPLATE") != "" {
Default.ClashTemplate = os.Getenv("CLASH_TEMPLATE")
}
if os.Getenv("REQUEST_RETRY_TIMES") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES"))
if err != nil {
panic("REQUEST_RETRY_TIMES invalid")
} }
Default.RequestRetryTimes = atoi
} }
if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE")) // 将配置解析到结构体
if err != nil { GlobalConfig = &Config{}
panic("REQUEST_MAX_FILE_SIZE invalid") if err := v.Unmarshal(GlobalConfig); err != nil {
} return err
Default.RequestMaxFileSize = int64(atoi)
}
if os.Getenv("CACHE_EXPIRE") != "" {
atoi, err := strconv.Atoi(os.Getenv("CACHE_EXPIRE"))
if err != nil {
panic("CACHE_EXPIRE invalid")
}
Default.CacheExpire = int64(atoi)
}
if os.Getenv("LOG_LEVEL") != "" {
Default.LogLevel = os.Getenv("LOG_LEVEL")
} }
return nil
}
func setDefaults(v *viper.Viper) {
v.SetDefault("address", "0.0.0.0:8011")
v.SetDefault("meta_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml")
v.SetDefault("clash_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml")
v.SetDefault("request_retry_times", 3)
v.SetDefault("request_max_file_size", 1024*1024*1)
v.SetDefault("cache_expire", 60*5)
v.SetDefault("log_level", "info")
v.SetDefault("short_link_length", 6)
} }
+3
View File
@@ -0,0 +1,3 @@
package constant
var Version = "dev"
+141 -22
View File
@@ -1,35 +1,154 @@
module sub2clash module github.com/bestnite/sub2clash
go 1.21 go 1.25
require ( require (
github.com/bytedance/sonic v1.10.1 // indirect github.com/gin-gonic/gin v1.10.1
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/glebarez/sqlite v1.11.0
github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/metacubex/mihomo v1.19.10
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/spf13/viper v1.20.1
go.uber.org/zap v1.27.0
golang.org/x/text v0.30.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.31.0
resty.dev/v3 v3.0.0-beta.3
)
require (
github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.4 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.20.5 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.2 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing v0.5.3 // indirect
github.com/metacubex/sing-mux v0.3.2 // indirect
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
github.com/metacubex/sing-shadowsocks v0.2.10 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.4 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c // indirect
github.com/metacubex/sing-vmess v0.2.2 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
github.com/metacubex/utls v1.7.3 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/openacid/low v0.1.21 // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/samber/lo v1.50.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.5.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.13.0 // indirect golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/mod v0.28.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/net v0.44.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect golang.org/x/sync v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
) )
+351 -53
View File
@@ -1,96 +1,394 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/RyuaNerin/testingutil v0.1.0/go.mod h1:yTqj6Ta/ycHMPJHRyO12Mz3VrvTloWOsy23WOZH19AA=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/mihomo v1.19.10 h1:GXCOA1rJNfU5qYSvo+UBUFksh61M0tjPfvlZ1OsYtfs=
github.com/metacubex/mihomo v1.19.10/go.mod h1:ih7BKy1pfqSvPRqaCcuFFK4oNRIFyBotoHX0PbhF7SQ=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM=
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.10 h1:Pr7LDbjMANIQHl07zWgl1vDuhpsfDQUpZ8cX6DPabfg=
github.com/metacubex/sing-shadowsocks v0.2.10/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk=
github.com/metacubex/sing-shadowsocks2 v0.2.4 h1:Ec0x3hHR7xkld5Z09IGh16wtUUpBb2HgqZ9DExd8Q7s=
github.com/metacubex/sing-shadowsocks2 v0.2.4/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A=
github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I=
github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do=
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU=
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo=
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+36 -56
View File
@@ -1,72 +1,52 @@
package logger package logger
import ( import (
"os"
"strings"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"path/filepath" "gopkg.in/natefinch/lumberjack.v2"
"sub2clash/config"
"sub2clash/utils"
"sync"
"time"
) )
var ( var Logger *zap.Logger
Logger *zap.Logger
lock sync.Mutex
)
func init() { func InitLogger(logLevel string) {
buildLogger() logger := zap.New(buildZapCore(getZapLogLevel(logLevel)))
go rotateLogs() Logger = logger
} }
func buildLogger() { func buildZapCore(logLevel zapcore.Level) zapcore.Core {
lock.Lock() fileWriter := zapcore.AddSync(&lumberjack.Logger{
defer lock.Unlock() Filename: "logs/app.log",
var level zapcore.Level MaxSize: 500,
switch config.Default.LogLevel { MaxBackups: 3,
case "error": MaxAge: 28,
level = zap.ErrorLevel Compress: true,
})
consoleWriter := zapcore.AddSync(os.Stdout)
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
fileCore := zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, logLevel)
consoleCore := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, logLevel)
combinedCore := zapcore.NewTee(fileCore, consoleCore)
return combinedCore
}
func getZapLogLevel(logLevel string) zapcore.Level {
switch strings.ToLower(logLevel) {
case "debug": case "debug":
level = zap.DebugLevel return zap.DebugLevel
case "warn": case "warn":
level = zap.WarnLevel return zap.WarnLevel
case "error":
return zap.ErrorLevel
case "info": case "info":
level = zap.InfoLevel return zap.InfoLevel
default: default:
level = zap.InfoLevel return zap.InfoLevel
}
err := utils.MKDir("logs")
if err != nil {
panic("创建日志失败" + err.Error())
}
zapConfig := zap.NewProductionConfig()
zapConfig.Encoding = "console"
zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
zapConfig.OutputPaths = []string{"stdout", getLogFileName("info")}
zapConfig.ErrorOutputPaths = []string{"stderr", getLogFileName("error")}
zapConfig.Level = zap.NewAtomicLevelAt(level)
Logger, err = zapConfig.Build()
if err != nil {
panic("创建日志失败" + err.Error())
}
}
// 根据日期获得日志文件
func getLogFileName(name string) string {
return filepath.Join("logs", time.Now().Format("2006-01-02")+"-"+name+".log")
}
func rotateLogs() {
for {
now := time.Now()
nextMidnight := time.Date(
now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location(),
).Add(24 * time.Hour)
durationUntilMidnight := nextMidnight.Sub(now)
time.Sleep(durationUntilMidnight)
buildLogger()
} }
} }
+27 -52
View File
@@ -2,72 +2,47 @@ package main
import ( import (
_ "embed" _ "embed"
"io"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/config"
"github.com/bestnite/sub2clash/logger"
"github.com/bestnite/sub2clash/server"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
"io"
"os"
"path/filepath"
"strconv"
"sub2clash/api"
"sub2clash/config"
"sub2clash/logger"
"sub2clash/utils"
) )
//go:embed templates/template_meta.yaml
var templateMeta string
//go:embed templates/template_clash.yaml
var templateClash string
func writeTemplate(path string, template string) error {
tPath := filepath.Join(
"templates", path,
)
if _, err := os.Stat(tPath); os.IsNotExist(err) {
file, err := os.Create(tPath)
if err != nil {
return err
}
defer func(file *os.File) {
_ = file.Close()
}(file)
_, err = file.WriteString(template)
if err != nil {
return err
}
}
return nil
}
func init() { func init() {
if err := utils.MKDir("subs"); err != nil { var err error
os.Exit(1)
err = common.MkEssentialDir()
if err != nil {
logger.Logger.Panic("create essential dir failed", zap.Error(err))
} }
if err := utils.MKDir("templates"); err != nil {
os.Exit(1) err = config.LoadConfig()
}
if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { logger.InitLogger(config.GlobalConfig.LogLevel)
os.Exit(1) if err != nil {
} logger.Logger.Panic("load config failed", zap.Error(err))
if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil {
os.Exit(1)
} }
logger.Logger.Info("database connect success")
} }
func main() { func main() {
// 设置运行模式
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
// 关闭 Gin 的日志输出
gin.DefaultWriter = io.Discard gin.DefaultWriter = io.Discard
// 创建路由
r := gin.Default() r := gin.Default()
// 设置路由
api.SetRoute(r) server.SetRoute(r)
logger.Logger.Info("Server is running at http://localhost:" + strconv.Itoa(config.Default.Port)) logger.Logger.Info("server is running at " + config.GlobalConfig.Address)
err := r.Run(":" + strconv.Itoa(config.Default.Port)) err := r.Run(config.GlobalConfig.Address)
if err != nil { if err != nil {
logger.Logger.Error("Server run error", zap.Error(err)) logger.Logger.Error("server running failed", zap.Error(err))
return return
} }
} }
+29
View File
@@ -0,0 +1,29 @@
package model
import "github.com/bestnite/sub2clash/parser"
type ClashType int
const (
Clash ClashType = 1 + iota
ClashMeta
)
func GetSupportProxyTypes(clashType ClashType) map[string]bool {
supportProxyTypes := make(map[string]bool)
for _, parser := range parser.GetAllParsers() {
switch clashType {
case Clash:
if parser.SupportClash() {
supportProxyTypes[parser.GetType()] = true
}
case ClashMeta:
if parser.SupportMeta() {
supportProxyTypes[parser.GetType()] = true
}
}
}
return supportProxyTypes
}
+92
View File
@@ -0,0 +1,92 @@
package model
import (
"encoding/json"
"errors"
"net/url"
"strings"
"github.com/bestnite/sub2clash/utils"
"github.com/gin-gonic/gin"
)
type ConvertConfig struct {
ClashType ClashType `json:"clashType" binding:"required"`
Subs []string `json:"subscriptions" binding:""`
Proxies []string `json:"proxies" binding:""`
Refresh bool `json:"refresh" binding:""`
Template string `json:"template" binding:""`
RuleProviders []RuleProviderStruct `json:"ruleProviders" binding:""`
Rules []RuleStruct `json:"rules" binding:""`
AutoTest bool `json:"autoTest" binding:""`
Lazy bool `json:"lazy" binding:""`
Sort string `json:"sort" binding:""`
Remove string `json:"remove" binding:""`
Replace map[string]string `json:"replace" binding:""`
NodeListMode bool `json:"nodeList" binding:""`
IgnoreCountryGrooup bool `json:"ignoreCountryGroup" binding:""`
UserAgent string `json:"userAgent" binding:""`
UseUDP bool `json:"useUDP" binding:""`
}
type RuleProviderStruct struct {
Behavior string `json:"behavior" binding:""`
Url string `json:"url" binding:""`
Group string `json:"group" binding:""`
Prepend bool `json:"prepend" binding:""`
Name string `json:"name" binding:""`
}
type RuleStruct struct {
Rule string `json:"rule" binding:""`
Prepend bool `json:"prepend" binding:""`
}
func ParseConvertQuery(c *gin.Context) (ConvertConfig, error) {
config := c.Param("config")
queryBytes, err := utils.DecodeBase64(config, true)
if err != nil {
return ConvertConfig{}, errors.New("参数错误: " + err.Error())
}
var query ConvertConfig
err = json.Unmarshal([]byte(queryBytes), &query)
if err != nil {
return ConvertConfig{}, errors.New("参数错误: " + err.Error())
}
if len(query.Subs) == 0 && len(query.Proxies) == 0 {
return ConvertConfig{}, errors.New("参数错误: sub 和 proxy 不能同时为空")
}
if len(query.Subs) > 0 {
for i := range query.Subs {
if !strings.HasPrefix(query.Subs[i], "http") {
return ConvertConfig{}, errors.New("参数错误: sub 格式错误")
}
if _, err := url.ParseRequestURI(query.Subs[i]); err != nil {
return ConvertConfig{}, errors.New("参数错误: " + err.Error())
}
}
} else {
query.Subs = nil
}
if query.Template != "" {
if strings.HasPrefix(query.Template, "http") {
uri, err := url.ParseRequestURI(query.Template)
if err != nil {
return ConvertConfig{}, err
}
query.Template = uri.String()
}
}
if len(query.RuleProviders) > 0 {
names := make(map[string]bool)
for _, ruleProvider := range query.RuleProviders {
if _, ok := names[ruleProvider.Name]; ok {
return ConvertConfig{}, errors.New("参数错误: Rule-Provider 名称重复")
}
names[ruleProvider.Name] = true
}
} else {
query.RuleProviders = nil
}
return query, nil
}
@@ -1,8 +1,5 @@
package model package model
// https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9F%9F%E6%8C%87%E7%A4%BA%E7%AC%A6
// https://zh.wikipedia.org/zh-sg/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81
var CountryEnglishName = map[string]string{ var CountryEnglishName = map[string]string{
"Andorra": "安道尔(AD)", "Andorra": "安道尔(AD)",
"United Arab Emirates": "阿联酋(AE)", "United Arab Emirates": "阿联酋(AE)",
-83
View File
@@ -1,83 +0,0 @@
package model
type PluginOptsStruct struct {
Mode string `yaml:"mode"`
}
type SmuxStruct struct {
Enabled bool `yaml:"enable"`
}
type HeaderStruct struct {
Host string `yaml:"Host"`
}
type WSOptsStruct struct {
Path string `yaml:"path,omitempty"`
Headers HeaderStruct `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
}
type Vmess struct {
V string `json:"v"`
Ps string `json:"ps"`
Add string `json:"add"`
Port string `json:"port"`
Id string `json:"id"`
Aid string `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
Tls string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
Fp string `json:"fp"`
}
type GRPCOptsStruct struct {
GRPCServiceName string `yaml:"grpc-service-name,omitempty"`
}
type RealityOptsStruct struct {
PublicKey string `yaml:"public-key,omitempty"`
ShortId string `yaml:"short-id,omitempty"`
}
type Proxy struct {
Name string `yaml:"name,omitempty"`
Server string `yaml:"server,omitempty"`
Port int `yaml:"port,omitempty"`
Type string `yaml:"type,omitempty"`
Cipher string `yaml:"cipher,omitempty"`
Password string `yaml:"password,omitempty"`
UDP bool `yaml:"udp,omitempty"`
UUID string `yaml:"uuid,omitempty"`
Network string `yaml:"network,omitempty"`
Flow string `yaml:"flow,omitempty"`
TLS bool `yaml:"tls,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
UdpOverTcp bool `yaml:"udp-over-tcp,omitempty"`
UdpOverTcpVersion string `yaml:"udp-over-tcp-version,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts PluginOptsStruct `yaml:"plugin-opts,omitempty"`
Smux SmuxStruct `yaml:"smux,omitempty"`
Sni string `yaml:"sni,omitempty"`
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Alpn []string `yaml:"alpn,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
Servername string `yaml:"servername,omitempty"`
WSOpts WSOptsStruct `yaml:"ws-opts,omitempty"`
AlterID string `yaml:"alterId,omitempty"`
GRPCOpts GRPCOptsStruct `yaml:"grpc-opts,omitempty"`
RealityOpts RealityOptsStruct `yaml:"reality-opts,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsParam string `yaml:"obfs-param,omitempty"`
ProtocolParam string `yaml:"protocol-param,omitempty"`
Remarks []string `yaml:"remarks,omitempty"`
}
+18
View File
@@ -0,0 +1,18 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/anytls.go
type Anytls struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
Password string `yaml:"password"`
ALPN []string `yaml:"alpn,omitempty"`
SNI string `yaml:"sni,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
UDP bool `yaml:"udp,omitempty"`
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
MinIdleSession int `yaml:"min-idle-session,omitempty"`
}
+29
View File
@@ -0,0 +1,29 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/hysteria.go
type Hysteria struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port,omitempty"`
Ports string `yaml:"ports,omitempty"`
Protocol string `yaml:"protocol,omitempty"`
ObfsProtocol string `yaml:"obfs-protocol,omitempty"` // compatible with Stash
Up string `yaml:"up"`
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
Down string `yaml:"down"`
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
Auth string `yaml:"auth,omitempty"`
AuthString string `yaml:"auth-str,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
SNI string `yaml:"sni,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
ReceiveWindow int `yaml:"recv-window,omitempty"`
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
FastOpen bool `yaml:"fast-open,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
}
+29
View File
@@ -0,0 +1,29 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/hysteria2.go
type Hysteria2 struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port,omitempty"`
Ports string `yaml:"ports,omitempty"`
HopInterval int `yaml:"hop-interval,omitempty"`
Up string `yaml:"up,omitempty"`
Down string `yaml:"down,omitempty"`
Password string `yaml:"password,omitempty"`
Obfs string `yaml:"obfs,omitempty"`
ObfsPassword string `yaml:"obfs-password,omitempty"`
SNI string `yaml:"sni,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
CustomCA string `yaml:"ca,omitempty"`
CustomCAString string `yaml:"ca-str,omitempty"`
CWND int `yaml:"cwnd,omitempty"`
UdpMTU int `yaml:"udp-mtu,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window,omitempty"`
MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window,omitempty"`
InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window,omitempty"`
MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window,omitempty"`
}
+325
View File
@@ -0,0 +1,325 @@
package proxy
import (
"fmt"
"strconv"
"gopkg.in/yaml.v3"
)
type IntOrString int
func (i *IntOrString) UnmarshalYAML(value *yaml.Node) error {
intVal := 0
err := yaml.Unmarshal([]byte(value.Value), &intVal)
if err == nil {
*i = IntOrString(intVal)
}
strVal := ""
err = yaml.Unmarshal([]byte(value.Value), &strVal)
if err == nil {
_int, err := strconv.ParseInt(strVal, 10, 64)
if err != nil {
*i = IntOrString(_int)
}
return err
}
return nil
}
type HTTPOptions struct {
Method string `yaml:"method,omitempty"`
Path []string `yaml:"path,omitempty"`
Headers map[string][]string `yaml:"headers,omitempty"`
}
type HTTP2Options struct {
Host []string `yaml:"host,omitempty"`
Path string `yaml:"path,omitempty"`
}
type GrpcOptions struct {
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
}
type RealityOptions struct {
PublicKey string `yaml:"public-key"`
ShortID string `yaml:"short-id,omitempty"`
}
type WSOptions struct {
Path string `yaml:"path,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
MaxEarlyData int `yaml:"max-early-data,omitempty"`
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
}
type SmuxStruct struct {
Enabled bool `yaml:"enable"`
}
type WireGuardPeerOption struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
PublicKey string `yaml:"public-key,omitempty"`
PreSharedKey string `yaml:"pre-shared-key,omitempty"`
Reserved []uint8 `yaml:"reserved,omitempty"`
AllowedIPs []string `yaml:"allowed-ips,omitempty"`
}
type ECHOptions struct {
Enable bool `yaml:"enable,omitempty" obfs:"enable,omitempty"`
Config string `yaml:"config,omitempty" obfs:"config,omitempty"`
}
type Proxy struct {
Type string
Name string
SubName string `yaml:"-"`
Anytls
Hysteria
Hysteria2
ShadowSocks
ShadowSocksR
Trojan
Vless
Vmess
Socks
Tuic
}
func (p Proxy) MarshalYAML() (any, error) {
switch p.Type {
case "anytls":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Anytls `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Anytls: p.Anytls,
}, nil
case "hysteria":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Hysteria: p.Hysteria,
}, nil
case "hysteria2":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria2 `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Hysteria2: p.Hysteria2,
}, nil
case "ss":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocks `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
ShadowSocks: p.ShadowSocks,
}, nil
case "ssr":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocksR `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
ShadowSocksR: p.ShadowSocksR,
}, nil
case "trojan":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Trojan `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Trojan: p.Trojan,
}, nil
case "vless":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vless `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Vless: p.Vless,
}, nil
case "vmess":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vmess `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Vmess: p.Vmess,
}, nil
case "socks5":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Socks `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Socks: p.Socks,
}, nil
case "tuic":
return struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Tuic `yaml:",inline"`
}{
Type: p.Type,
Name: p.Name,
Tuic: p.Tuic,
}, nil
default:
return nil, fmt.Errorf("unsupported proxy type: %s", p.Type)
}
}
func (p *Proxy) UnmarshalYAML(node *yaml.Node) error {
var temp struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
}
if err := node.Decode(&temp); err != nil {
return err
}
p.Type = temp.Type
p.Name = temp.Name
switch temp.Type {
case "anytls":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Anytls `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Anytls = data.Anytls
case "hysteria":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Hysteria = data.Hysteria
case "hysteria2":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Hysteria2 `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Hysteria2 = data.Hysteria2
case "ss":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocks `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.ShadowSocks = data.ShadowSocks
case "ssr":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
ShadowSocksR `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.ShadowSocksR = data.ShadowSocksR
case "trojan":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Trojan `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Trojan = data.Trojan
case "vless":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vless `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Vless = data.Vless
case "vmess":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Vmess `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Vmess = data.Vmess
case "socks5":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Socks `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Socks = data.Socks
case "tuic":
var data struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Tuic `yaml:",inline"`
}
if err := node.Decode(&data); err != nil {
return err
}
p.Tuic = data.Tuic
default:
return fmt.Errorf("unsupported proxy type: %s", temp.Type)
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/shadowsocks.go
type ShadowSocks struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
Password string `yaml:"password"`
Cipher string `yaml:"cipher"`
UDP bool `yaml:"udp,omitempty"`
Plugin string `yaml:"plugin,omitempty"`
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
+14
View File
@@ -0,0 +1,14 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/shadowsocksr.go
type ShadowSocksR struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
Password string `yaml:"password"`
Cipher string `yaml:"cipher"`
Obfs string `yaml:"obfs"`
ObfsParam string `yaml:"obfs-param,omitempty"`
Protocol string `yaml:"protocol"`
ProtocolParam string `yaml:"protocol-param,omitempty"`
UDP bool `yaml:"udp,omitempty"`
}
+13
View File
@@ -0,0 +1,13 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/socks5.go
type Socks struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
UserName string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
TLS bool `yaml:"tls,omitempty"`
UDP bool `yaml:"udp,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
}
+26
View File
@@ -0,0 +1,26 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/trojan.go
type Trojan struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
Password string `yaml:"password"`
ALPN []string `yaml:"alpn,omitempty"`
SNI string `yaml:"sni,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
type TrojanSSOption struct {
Enabled bool `yaml:"enabled,omitempty"`
Method string `yaml:"method,omitempty"`
Password string `yaml:"password,omitempty"`
}
+35
View File
@@ -0,0 +1,35 @@
package proxy
type Tuic struct {
Server string `proxy:"server"`
Port int `proxy:"port"`
Token string `proxy:"token,omitempty"`
UUID string `proxy:"uuid,omitempty"`
Password string `proxy:"password,omitempty"`
Ip string `proxy:"ip,omitempty"`
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
ReduceRtt bool `proxy:"reduce-rtt,omitempty"`
RequestTimeout int `proxy:"request-timeout,omitempty"`
UdpRelayMode string `proxy:"udp-relay-mode,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
DisableSni bool `proxy:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Certificate string `proxy:"certificate,omitempty"`
PrivateKey string `proxy:"private-key,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
}
+28
View File
@@ -0,0 +1,28 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/vless.go
type Vless struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
UUID string `yaml:"uuid"`
Flow string `yaml:"flow,omitempty"`
TLS bool `yaml:"tls,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
UDP bool `yaml:"udp,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
Network string `yaml:"network,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
WSPath string `yaml:"ws-path,omitempty"`
WSHeaders map[string]string `yaml:"ws-headers,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ServerName string `yaml:"servername,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
+29
View File
@@ -0,0 +1,29 @@
package proxy
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/vmess.go
type Vmess struct {
Server string `yaml:"server"`
Port IntOrString `yaml:"port"`
UUID string `yaml:"uuid"`
AlterID IntOrString `yaml:"alterId"`
Cipher string `yaml:"cipher"`
UDP bool `yaml:"udp,omitempty"`
Network string `yaml:"network,omitempty"`
TLS bool `yaml:"tls,omitempty"`
ALPN []string `yaml:"alpn,omitempty"`
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
Fingerprint string `yaml:"fingerprint,omitempty"`
ServerName string `yaml:"servername,omitempty"`
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
PacketAddr bool `yaml:"packet-addr,omitempty"`
XUDP bool `yaml:"xudp,omitempty"`
PacketEncoding string `yaml:"packet-encoding,omitempty"`
GlobalPadding bool `yaml:"global-padding,omitempty"`
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
}
+8
View File
@@ -0,0 +1,8 @@
package model
type ShortLink struct {
ID string `gorm:"unique"`
Config ConvertConfig `gorm:"serializer:json"`
Password string
LastRequestTime int64
}
-37
View File
@@ -1,37 +0,0 @@
package model
type Subscription struct {
Port int `yaml:"port,omitempty"`
SocksPort int `yaml:"socks-port,omitempty"`
AllowLan bool `yaml:"allow-lan"`
Mode string `yaml:"mode,omitempty"`
LogLevel string `yaml:"logger-level,omitempty"`
ExternalController string `yaml:"external-controller,omitempty"`
Proxies []Proxy `yaml:"proxies,omitempty"`
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
Rules []string `yaml:"rules,omitempty"`
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
}
type ProxyGroup struct {
Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"`
IsCountryGrop bool `yaml:"-"`
Url string `yaml:"url,omitempty"`
Interval int `yaml:"interval,omitempty"`
Tolerance int `yaml:"tolerance,omitempty"`
Lazy bool `yaml:"lazy"`
}
type RuleProvider struct {
Type string `yaml:"type,omitempty"`
Behavior string `yaml:"behavior,omitempty"`
Url string `yaml:"url,omitempty"`
Path string `yaml:"path,omitempty"`
Interval int `yaml:"interval,omitempty"`
}
type Payload struct {
Rules []string `yaml:"payload,omitempty"`
}
+83
View File
@@ -0,0 +1,83 @@
package parser
import (
"fmt"
"net/url"
"strings"
P "github.com/bestnite/sub2clash/model/proxy"
)
type AnytlsParser struct{}
func (p *AnytlsParser) SupportClash() bool {
return false
}
func (p *AnytlsParser) SupportMeta() bool {
return true
}
func (p *AnytlsParser) GetPrefixes() []string {
return []string{"anytls://"}
}
func (p *AnytlsParser) GetType() string {
return "anytls"
}
func (p *AnytlsParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
username := link.User.Username()
password, exist := link.User.Password()
if !exist {
password = username
}
query := link.Query()
server := link.Hostname()
if server == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
insecure, sni := query.Get("insecure"), query.Get("sni")
insecureBool := insecure == "1"
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Anytls: P.Anytls{
Server: server,
Port: P.IntOrString(port),
Password: password,
SNI: sni,
SkipCertVerify: insecureBool,
UDP: config.UseUDP,
},
}
return result, nil
}
func init() {
RegisterParser(&AnytlsParser{})
}
-13
View File
@@ -1,13 +0,0 @@
package parser
import (
"encoding/base64"
)
func DecodeBase64(s string) (string, error) {
decodeStr, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(decodeStr), nil
}
+82
View File
@@ -0,0 +1,82 @@
package parser
import (
"errors"
"strconv"
"strings"
"unicode/utf8"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
func hasPrefix(proxy string, prefixes []string) bool {
hasPrefix := false
for _, prefix := range prefixes {
if strings.HasPrefix(proxy, prefix) {
hasPrefix = true
break
}
}
return hasPrefix
}
func ParsePort(portStr string) (int, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, errors.New("invaild port range")
}
return port, nil
}
// isLikelyBase64 不严格判断是否是合法的 Base64, 很多分享链接不符合 Base64 规范
func isLikelyBase64(s string) bool {
if strings.TrimSpace(s) == "" {
return false
}
if !strings.Contains(strings.TrimSuffix(s, "="), "=") {
s = strings.TrimSuffix(s, "=")
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for _, c := range s {
if !strings.ContainsRune(chars, c) {
return false
}
}
}
decoded, err := utils.DecodeBase64(s, true)
if err != nil {
return false
}
if !utf8.ValidString(decoded) {
return false
}
return true
}
type ParseConfig struct {
UseUDP bool
}
func ParseProxies(config ParseConfig, proxies ...string) ([]P.Proxy, error) {
var result []P.Proxy
for _, proxy := range proxies {
if proxy != "" {
var proxyItem P.Proxy
var err error
proxyItem, err = ParseProxyWithRegistry(config, proxy)
if err != nil {
return nil, err
}
result = append(result, proxyItem)
}
}
return result, nil
}
+15
View File
@@ -0,0 +1,15 @@
package parser
type ParseErrorType string
const (
ErrInvalidPrefix ParseErrorType = "invalid url prefix"
ErrInvalidStruct ParseErrorType = "invalid struct"
ErrInvalidPort ParseErrorType = "invalid port number"
ErrCannotParseParams ParseErrorType = "cannot parse query parameters"
ErrInvalidBase64 ParseErrorType = "invalid base64"
)
func (e ParseErrorType) Error() string {
return string(e)
}
+95
View File
@@ -0,0 +1,95 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"strings"
P "github.com/bestnite/sub2clash/model/proxy"
)
type HysteriaParser struct{}
func (p *HysteriaParser) SupportClash() bool {
return false
}
func (p *HysteriaParser) SupportMeta() bool {
return true
}
func (p *HysteriaParser) GetPrefixes() []string {
return []string{"hysteria://"}
}
func (p *HysteriaParser) GetType() string {
return "hysteria"
}
func (p *HysteriaParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
query := link.Query()
protocol, auth, auth_str, insecure, upmbps, downmbps, obfs, alpnStr := query.Get("protocol"), query.Get("auth"), query.Get("auth-str"), query.Get("insecure"), query.Get("upmbps"), query.Get("downmbps"), query.Get("obfs"), query.Get("alpn")
insecureBool, err := strconv.ParseBool(insecure)
if err != nil {
insecureBool = false
}
var alpn []string
alpnStr = strings.TrimSpace(alpnStr)
if alpnStr != "" {
alpn = strings.Split(alpnStr, ",")
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Hysteria: P.Hysteria{
Server: server,
Port: P.IntOrString(port),
Up: upmbps,
Down: downmbps,
Auth: auth,
AuthString: auth_str,
Obfs: obfs,
SkipCertVerify: insecureBool,
ALPN: alpn,
Protocol: protocol,
},
}
return result, nil
}
func init() {
RegisterParser(&HysteriaParser{})
}
+84
View File
@@ -0,0 +1,84 @@
package parser
import (
"fmt"
"net/url"
"strings"
P "github.com/bestnite/sub2clash/model/proxy"
)
type Hysteria2Parser struct{}
func (p *Hysteria2Parser) SupportClash() bool {
return false
}
func (p *Hysteria2Parser) SupportMeta() bool {
return true
}
func (p *Hysteria2Parser) GetPrefixes() []string {
return []string{"hysteria2://", "hy2://"}
}
func (p *Hysteria2Parser) GetType() string {
return "hysteria2"
}
func (p *Hysteria2Parser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
username := link.User.Username()
password, exist := link.User.Password()
if !exist {
password = username
}
query := link.Query()
server := link.Hostname()
if server == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
obfs, obfsPassword, insecure, sni := query.Get("obfs"), query.Get("obfs-password"), query.Get("insecure"), query.Get("sni")
insecureBool := insecure == "1"
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
Hysteria2: P.Hysteria2{
Server: server,
Port: P.IntOrString(port),
Password: password,
Obfs: obfs,
ObfsPassword: obfsPassword,
SNI: sni,
SkipCertVerify: insecureBool,
},
}
return result, nil
}
func init() {
RegisterParser(&Hysteria2Parser{})
}
+80
View File
@@ -0,0 +1,80 @@
package parser
import (
"fmt"
"strings"
"sync"
P "github.com/bestnite/sub2clash/model/proxy"
)
type ProxyParser interface {
Parse(config ParseConfig, proxy string) (P.Proxy, error)
GetPrefixes() []string
GetType() string
SupportClash() bool
SupportMeta() bool
}
type parserRegistry struct {
mu sync.RWMutex
parsers map[string]ProxyParser
}
var registry = &parserRegistry{
parsers: make(map[string]ProxyParser),
}
func RegisterParser(parser ProxyParser) {
registry.mu.Lock()
defer registry.mu.Unlock()
for _, prefix := range parser.GetPrefixes() {
registry.parsers[prefix] = parser
}
}
func GetParser(prefix string) (ProxyParser, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
parser, exists := registry.parsers[prefix]
return parser, exists
}
func GetAllParsers() map[string]ProxyParser {
registry.mu.RLock()
defer registry.mu.RUnlock()
result := make(map[string]ProxyParser)
for k, v := range registry.parsers {
result[k] = v
}
return result
}
func GetAllPrefixes() []string {
registry.mu.RLock()
defer registry.mu.RUnlock()
prefixes := make([]string, 0, len(registry.parsers))
for prefix := range registry.parsers {
prefixes = append(prefixes, prefix)
}
return prefixes
}
func ParseProxyWithRegistry(config ParseConfig, proxy string) (P.Proxy, error) {
proxy = strings.TrimSpace(proxy)
if proxy == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "empty proxy string")
}
for prefix, parser := range registry.parsers {
if strings.HasPrefix(proxy, prefix) {
return parser.Parse(config, proxy)
}
}
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, "unsupported protocol")
}
+121
View File
@@ -0,0 +1,121 @@
package parser
import (
"fmt"
"net/url"
"strings"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
// ShadowsocksParser Shadowsocks协议解析器
type ShadowsocksParser struct{}
func (p *ShadowsocksParser) SupportClash() bool {
return true
}
func (p *ShadowsocksParser) SupportMeta() bool {
return true
}
// GetPrefixes 返回支持的协议前缀
func (p *ShadowsocksParser) GetPrefixes() []string {
return []string{"ss://"}
}
// GetType 返回协议类型
func (p *ShadowsocksParser) GetType() string {
return "ss"
}
// Parse 解析Shadowsocks代理
func (p *ShadowsocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
if !strings.Contains(proxy, "@") {
s := strings.SplitN(proxy, "#", 2)
for _, prefix := range p.GetPrefixes() {
if strings.HasPrefix(s[0], prefix) {
s[0] = strings.TrimPrefix(s[0], prefix)
break
}
}
d, err := utils.DecodeBase64(s[0], true)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
if len(s) == 2 {
proxy = "ss://" + d + "#" + s[1]
} else {
proxy = "ss://" + d
}
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
method := link.User.Username()
password, hasPassword := link.User.Password()
if !hasPassword && isLikelyBase64(method) {
decodedStr, err := utils.DecodeBase64(method, true)
if err == nil {
methodAndPass := strings.SplitN(decodedStr, ":", 2)
if len(methodAndPass) == 2 {
method = methodAndPass[0]
password = methodAndPass[1]
} else {
method = decodedStr
}
}
}
if password != "" && isLikelyBase64(password) {
password, err = utils.DecodeBase64(password, true)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
ShadowSocks: P.ShadowSocks{
Cipher: method,
Password: password,
Server: server,
Port: P.IntOrString(port),
UDP: config.UseUDP,
},
}
return result, nil
}
// 注册解析器
func init() {
RegisterParser(&ShadowsocksParser{})
}
+135
View File
@@ -0,0 +1,135 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"strings"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
type ShadowsocksRParser struct{}
func (p *ShadowsocksRParser) SupportClash() bool {
return true
}
func (p *ShadowsocksRParser) SupportMeta() bool {
return true
}
func (p *ShadowsocksRParser) GetPrefixes() []string {
return []string{"ssr://"}
}
func (p *ShadowsocksRParser) GetType() string {
return "ssr"
}
func (p *ShadowsocksRParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
for _, prefix := range p.GetPrefixes() {
if strings.HasPrefix(proxy, prefix) {
proxy = strings.TrimPrefix(proxy, prefix)
break
}
}
proxy, err := utils.DecodeBase64(proxy, true)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error())
}
serverInfoAndParams := strings.SplitN(proxy, "/?", 2)
if len(serverInfoAndParams) != 2 {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, proxy)
}
parts := SplitNRight(serverInfoAndParams[0], ":", 6)
if len(parts) < 6 {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, proxy)
}
server := parts[0]
protocol := parts[2]
method := parts[3]
obfs := parts[4]
password, err := utils.DecodeBase64(parts[5], true)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
port, err := ParsePort(parts[1])
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
var obfsParam string
var protoParam string
var remarks string
if len(serverInfoAndParams) == 2 {
params, err := url.ParseQuery(serverInfoAndParams[1])
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrCannotParseParams, err.Error())
}
if params.Get("obfsparam") != "" {
obfsParam, err = utils.DecodeBase64(params.Get("obfsparam"), true)
}
if params.Get("protoparam") != "" {
protoParam, err = utils.DecodeBase64(params.Get("protoparam"), true)
}
if params.Get("remarks") != "" {
remarks, err = utils.DecodeBase64(params.Get("remarks"), true)
} else {
remarks = server + ":" + strconv.Itoa(port)
}
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
}
result := P.Proxy{
Type: p.GetType(),
Name: remarks,
ShadowSocksR: P.ShadowSocksR{
Server: server,
Port: P.IntOrString(port),
Protocol: protocol,
Cipher: method,
Obfs: obfs,
Password: password,
ObfsParam: obfsParam,
ProtocolParam: protoParam,
UDP: config.UseUDP,
},
}
return result, nil
}
func init() {
RegisterParser(&ShadowsocksRParser{})
}
func SplitNRight(s, sep string, n int) []string {
if n <= 0 {
return nil
}
if n == 1 {
return []string{s}
}
parts := strings.Split(s, sep)
if len(parts) <= n {
return parts
}
result := make([]string, n)
for i, j := len(parts)-1, 0; i >= 0; i, j = i-1, j+1 {
if j < n-1 {
result[n-j-1] = parts[len(parts)-j-1]
} else {
result[0] = strings.Join(parts[:i+1], sep)
break
}
}
return result
}
+93
View File
@@ -0,0 +1,93 @@
package parser
import (
"fmt"
"net/url"
"strings"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
)
type SocksParser struct{}
func (p *SocksParser) SupportClash() bool {
return true
}
func (p *SocksParser) SupportMeta() bool {
return true
}
func (p *SocksParser) GetPrefixes() []string {
return []string{"socks://", "socks5://"}
}
func (p *SocksParser) GetType() string {
return "socks5"
}
func (p *SocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
}
link, err := url.Parse(proxy)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
server := link.Hostname()
if server == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
}
portStr := link.Port()
if portStr == "" {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
}
port, err := ParsePort(portStr)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
var username, password string
username = link.User.Username()
password, hasPassword := link.User.Password()
if !hasPassword && isLikelyBase64(username) {
decodedStr, err := utils.DecodeBase64(username, true)
if err == nil {
usernameAndPassword := strings.SplitN(decodedStr, ":", 2)
if len(usernameAndPassword) == 2 {
username = usernameAndPassword[0]
password = usernameAndPassword[1]
} else {
username = decodedStr
}
}
}
tls, udp := link.Query().Get("tls"), link.Query().Get("udp")
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Socks: P.Socks{
Server: server,
Port: P.IntOrString(port),
UserName: username,
Password: password,
TLS: tls == "true",
UDP: udp == "true" || config.UseUDP,
},
}, nil
}
func init() {
RegisterParser(&SocksParser{})
}
-67
View File
@@ -1,67 +0,0 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"strings"
"sub2clash/model"
)
// ParseSS 解析 SSShadowsocksUrl
func ParseSS(proxy string) (model.Proxy, error) {
// 判断是否以 ss:// 开头
if !strings.HasPrefix(proxy, "ss://") {
return model.Proxy{}, fmt.Errorf("无效的 ss Url")
}
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2)
if len(parts) != 2 {
return model.Proxy{}, fmt.Errorf("无效的 ss Url")
}
if !strings.Contains(parts[0], ":") {
// 解码
decoded, err := DecodeBase64(parts[0])
if err != nil {
return model.Proxy{}, err
}
parts[0] = decoded
}
credentials := strings.SplitN(parts[0], ":", 2)
if len(credentials) != 2 {
return model.Proxy{}, fmt.Errorf("无效的 ss 凭证")
}
// 分割
serverInfo := strings.SplitN(parts[1], "#", 2)
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
if len(serverAndPort) != 2 {
return model.Proxy{}, fmt.Errorf("无效的 ss 服务器和端口")
}
// 转换端口字符串为数字
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
if err != nil {
return model.Proxy{}, err
}
// 返回结果
result := model.Proxy{
Type: "ss",
Cipher: strings.TrimSpace(credentials[0]),
Password: strings.TrimSpace(credentials[1]),
Server: strings.TrimSpace(serverAndPort[0]),
Port: port,
UDP: true,
Name: serverAndPort[0],
}
// 如果有节点名称
if len(serverInfo) == 2 {
unescape, err := url.QueryUnescape(serverInfo[1])
if err != nil {
return model.Proxy{}, err
}
result.Name = strings.TrimSpace(unescape)
} else {
result.Name = strings.TrimSpace(serverAndPort[0])
}
return result, nil
}
-47
View File
@@ -1,47 +0,0 @@
package parser
import (
"fmt"
"net/url"
"strconv"
"strings"
"sub2clash/model"
)
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
// 判断是否以 ssr:// 开头
if !strings.HasPrefix(proxy, "ssr://") {
return model.Proxy{}, fmt.Errorf("无效的 ssr Url")
}
var err error
if !strings.Contains(proxy, ":") {
proxy, err = DecodeBase64(strings.TrimPrefix(proxy, "ssr://"))
if err != nil {
return model.Proxy{}, err
}
}
// 分割
detailsAndParams := strings.SplitN(strings.TrimPrefix(proxy, "ssr://"), "/?", 2)
parts := strings.Split(detailsAndParams[0], ":")
params, err := url.ParseQuery(detailsAndParams[1])
if err != nil {
return model.Proxy{}, err
}
// 处理端口
port, err := strconv.Atoi(parts[1])
if err != nil {
return model.Proxy{}, err
}
result := model.Proxy{
Type: "ssr",
Server: parts[0],
Port: port,
Protocol: parts[2],
Cipher: parts[3],
Obfs: parts[4],
Password: parts[5],
ObfsParam: params.Get("obfsparam"),
ProtocolParam: params.Get("protoparam"),
}
return result, nil
}
+114 -44
View File
@@ -3,51 +3,121 @@ package parser
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sub2clash/model"
P "github.com/bestnite/sub2clash/model/proxy"
) )
func ParseTrojan(proxy string) (model.Proxy, error) { type TrojanParser struct{}
// 判断是否以 trojan:// 开头
if !strings.HasPrefix(proxy, "trojan://") { func (p *TrojanParser) SupportClash() bool {
return model.Proxy{}, fmt.Errorf("无效的 trojan Url") return true
} }
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) func (p *TrojanParser) SupportMeta() bool {
if len(parts) != 2 { return true
return model.Proxy{}, fmt.Errorf("无效的 trojan Url") }
}
// 分割 func (p *TrojanParser) GetPrefixes() []string {
serverInfo := strings.SplitN(parts[1], "#", 2) return []string{"trojan://"}
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) }
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
params, err := url.ParseQuery(serverAndPortAndParams[1]) func (p *TrojanParser) GetType() string {
if err != nil { return "trojan"
return model.Proxy{}, err }
}
if len(serverAndPort) != 2 { func (p *TrojanParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
return model.Proxy{}, fmt.Errorf("无效的 trojan 服务器和端口") if !hasPrefix(proxy, p.GetPrefixes()) {
} return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
// 处理端口 }
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
if err != nil { link, err := url.Parse(proxy)
return model.Proxy{}, err if err != nil {
} return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
// 返回结果 }
result := model.Proxy{
Type: "trojan", password := link.User.Username()
Server: strings.TrimSpace(serverAndPort[0]), server := link.Hostname()
Port: port, if server == "" {
UDP: true, return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
Password: strings.TrimSpace(parts[0]), }
Sni: params.Get("sni"), portStr := link.Port()
} if portStr == "" {
// 如果有节点名称 return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
if len(serverInfo) == 2 { }
result.Name, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
} else { port, err := ParsePort(portStr)
result.Name = serverAndPort[0] if err != nil {
} return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
return result, nil }
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
remarks = strings.TrimSpace(remarks)
query := link.Query()
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName, udp, insecure := query.Get("type"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("pbk"), query.Get("sid"), query.Get("fp"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("udp"), query.Get("allowInsecure")
insecureBool := insecure == "1"
result := P.Trojan{
Server: server,
Port: P.IntOrString(port),
Password: password,
Network: network,
UDP: udp == "true" || config.UseUDP,
SkipCertVerify: insecureBool,
}
var alpn []string
if strings.Contains(alpnStr, ",") {
alpn = strings.Split(alpnStr, ",")
} else {
alpn = nil
}
if len(alpn) > 0 {
result.ALPN = alpn
}
if fp != "" {
result.ClientFingerprint = fp
}
if sni != "" {
result.SNI = sni
}
if security == "reality" {
result.RealityOpts = P.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
}
if network == "ws" {
result.Network = "ws"
result.WSOpts = P.WSOptions{
Path: path,
Headers: map[string]string{
"Host": host,
},
}
}
if network == "grpc" {
result.GrpcOpts = P.GrpcOptions{
GrpcServiceName: serviceName,
}
}
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Trojan: result,
}, nil
}
func init() {
RegisterParser(&TrojanParser{})
} }
+134 -72
View File
@@ -3,79 +3,141 @@ package parser
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sub2clash/model"
P "github.com/bestnite/sub2clash/model/proxy"
) )
func ParseVless(proxy string) (model.Proxy, error) { type VlessParser struct{}
// 判断是否以 vless:// 开头
if !strings.HasPrefix(proxy, "vless://") { func (p *VlessParser) SupportClash() bool {
return model.Proxy{}, fmt.Errorf("无效的 vless Url") return false
} }
// 分割
parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) func (p *VlessParser) SupportMeta() bool {
if len(parts) != 2 { return true
return model.Proxy{}, fmt.Errorf("无效的 vless Url") }
}
// 分割 func (p *VlessParser) GetPrefixes() []string {
serverInfo := strings.SplitN(parts[1], "#", 2) return []string{"vless://"}
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) }
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
params, err := url.ParseQuery(serverAndPortAndParams[1]) func (p *VlessParser) GetType() string {
if err != nil { return "vless"
return model.Proxy{}, err }
}
if len(serverAndPort) != 2 { func (p *VlessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
return model.Proxy{}, fmt.Errorf("无效的 vless 服务器和端口") if !hasPrefix(proxy, p.GetPrefixes()) {
} return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
// 处理端口 }
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
if err != nil { link, err := url.Parse(proxy)
return model.Proxy{}, err if err != nil {
} return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
// 返回结果 }
result := model.Proxy{
Type: "vless", server := link.Hostname()
Server: strings.TrimSpace(serverAndPort[0]), if server == "" {
Port: port, return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
UUID: strings.TrimSpace(parts[0]), }
UDP: true, portStr := link.Port()
Sni: params.Get("sni"), port, err := ParsePort(portStr)
Network: params.Get("type"), if err != nil {
TLS: params.Get("security") == "tls", return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
Flow: params.Get("flow"), }
Fingerprint: params.Get("fp"),
Servername: params.Get("sni"), query := link.Query()
RealityOpts: model.RealityOptsStruct{ uuid := link.User.Username()
PublicKey: params.Get("pbk"), flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type, udp := query.Get("flow"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("allowInsecure"), query.Get("fp"), query.Get("pbk"), query.Get("sid"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("type"), query.Get("udp")
},
} insecureBool := insecure == "1"
if params.Get("alpn") != "" { var alpn []string
result.Alpn = strings.Split(params.Get("alpn"), ",") if strings.Contains(alpnStr, ",") {
} alpn = strings.Split(alpnStr, ",")
if params.Get("type") == "ws" { } else {
result.WSOpts = model.WSOptsStruct{ alpn = nil
Path: params.Get("path"), }
Headers: model.HeaderStruct{
Host: params.Get("host"), remarks := link.Fragment
}, if remarks == "" {
} remarks = fmt.Sprintf("%s:%s", server, portStr)
} }
if params.Get("type") == "grpc" { remarks = strings.TrimSpace(remarks)
result.GRPCOpts = model.GRPCOptsStruct{
GRPCServiceName: params.Get("serviceName"), result := P.Vless{
} Server: server,
} Port: P.IntOrString(port),
// 如果有节点名称 UUID: uuid,
if len(serverInfo) == 2 { Flow: flow,
if strings.Contains(serverInfo[1], "|") { UDP: udp == "true" || config.UseUDP,
result.Name = strings.SplitN(serverInfo[1], "|", 2)[1] SkipCertVerify: insecureBool,
} else { }
result.Name = serverInfo[1]
} if len(alpn) > 0 {
} else { result.ALPN = alpn
result.Name = serverAndPort[0] }
}
return result, nil if fp != "" {
result.ClientFingerprint = fp
}
if sni != "" {
result.ServerName = sni
}
if security == "tls" {
result.TLS = true
}
if security == "reality" {
result.TLS = true
result.RealityOpts = P.RealityOptions{
PublicKey: pbk,
ShortID: sid,
}
}
if _type == "ws" {
result.Network = "ws"
result.WSOpts = P.WSOptions{
Path: path,
}
if host != "" {
result.WSOpts.Headers = make(map[string]string)
result.WSOpts.Headers["Host"] = host
}
}
if _type == "grpc" {
result.Network = "grpc"
result.GrpcOpts = P.GrpcOptions{
GrpcServiceName: serviceName,
}
}
if _type == "http" {
result.HTTPOpts = P.HTTPOptions{}
result.HTTPOpts.Headers = map[string][]string{}
result.HTTPOpts.Path = strings.Split(path, ",")
hosts, err := url.QueryUnescape(host)
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrCannotParseParams, err.Error())
}
result.Network = "http"
if hosts != "" {
result.HTTPOpts.Headers["host"] = strings.Split(host, ",")
}
}
return P.Proxy{
Type: p.GetType(),
Name: remarks,
Vless: result,
}, nil
}
func init() {
RegisterParser(&VlessParser{})
} }
+147 -40
View File
@@ -2,67 +2,174 @@ package parser
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url"
"strconv" "strconv"
"strings" "strings"
"sub2clash/model"
P "github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/utils"
) )
func ParseVmess(proxy string) (model.Proxy, error) { type VmessJson struct {
// 判断是否以 vmess:// 开头 V any `json:"v"`
if !strings.HasPrefix(proxy, "vmess://") { Ps string `json:"ps"`
return model.Proxy{}, fmt.Errorf("无效的 vmess Url") Add string `json:"add"`
Port any `json:"port"`
Id string `json:"id"`
Aid any `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
Tls string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
Fp string `json:"fp"`
}
type VmessParser struct{}
func (p *VmessParser) SupportClash() bool {
return true
}
func (p *VmessParser) SupportMeta() bool {
return true
}
func (p *VmessParser) GetPrefixes() []string {
return []string{"vmess://"}
}
func (p *VmessParser) GetType() string {
return "vmess"
}
func (p *VmessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
if !hasPrefix(proxy, p.GetPrefixes()) {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
} }
// 解码
base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) for _, prefix := range p.GetPrefixes() {
if strings.HasPrefix(proxy, prefix) {
proxy = strings.TrimPrefix(proxy, prefix)
break
}
}
base64, err := utils.DecodeBase64(proxy, true)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("无效的 vmess Url") return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error())
} }
// 解析
var vmess model.Vmess var vmess VmessJson
err = json.Unmarshal([]byte(base64), &vmess) err = json.Unmarshal([]byte(base64), &vmess)
if err != nil { if err != nil {
return model.Proxy{}, errors.New("无效的 vmess Url") return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
} }
// 处理端口
port, err := strconv.Atoi(strings.TrimSpace(vmess.Port)) var port int
if err != nil { switch vmess.Port.(type) {
return model.Proxy{}, errors.New("无效的 vmess Url") case string:
port, err = ParsePort(vmess.Port.(string))
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
}
case float64:
port = int(vmess.Port.(float64))
} }
aid := 0
switch vmess.Aid.(type) {
case string:
aid, err = strconv.Atoi(vmess.Aid.(string))
if err != nil {
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
}
case float64:
aid = int(vmess.Aid.(float64))
}
if vmess.Scy == "" { if vmess.Scy == "" {
vmess.Scy = "auto" vmess.Scy = "auto"
} }
if vmess.Net == "ws" && vmess.Path == "" {
vmess.Path = "/" name, err := url.QueryUnescape(vmess.Ps)
if err != nil {
name = vmess.Ps
} }
if vmess.Net == "ws" && vmess.Host == "" {
vmess.Host = vmess.Add var alpn []string
if strings.Contains(vmess.Alpn, ",") {
alpn = strings.Split(vmess.Alpn, ",")
} else {
alpn = nil
} }
// 返回结果
result := model.Proxy{ result := P.Vmess{
Name: vmess.Ps, Server: vmess.Add,
Type: "vmess", Port: P.IntOrString(port),
Server: vmess.Add, UUID: vmess.Id,
Port: port, AlterID: P.IntOrString(aid),
UUID: vmess.Id, Cipher: vmess.Scy,
AlterID: vmess.Aid, UDP: config.UseUDP,
Cipher: vmess.Scy,
UDP: true,
TLS: vmess.Tls == "tls",
Fingerprint: vmess.Fp,
ClientFingerprint: "chrome",
SkipCertVerify: true,
Servername: vmess.Add,
Network: vmess.Net,
} }
if len(alpn) > 0 {
result.ALPN = alpn
}
if vmess.Fp != "" {
result.ClientFingerprint = vmess.Fp
}
if vmess.Sni != "" {
result.ServerName = vmess.Sni
}
if vmess.Tls == "tls" {
result.TLS = true
}
if vmess.Net == "ws" { if vmess.Net == "ws" {
result.WSOpts = model.WSOptsStruct{ if vmess.Path == "" {
vmess.Path = "/"
}
if vmess.Host == "" {
vmess.Host = vmess.Add
}
result.Network = "ws"
result.WSOpts = P.WSOptions{
Path: vmess.Path, Path: vmess.Path,
Headers: model.HeaderStruct{ Headers: map[string]string{
Host: vmess.Host, "Host": vmess.Host,
}, },
} }
} }
return result, nil
if vmess.Net == "grpc" {
result.GrpcOpts = P.GrpcOptions{
GrpcServiceName: vmess.Path,
}
result.Network = "grpc"
}
if vmess.Net == "h2" {
result.HTTP2Opts = P.HTTP2Options{
Host: strings.Split(vmess.Host, ","),
Path: vmess.Path,
}
result.Network = "h2"
}
return P.Proxy{
Type: p.GetType(),
Name: name,
Vmess: result,
}, nil
}
func init() {
RegisterParser(&VmessParser{})
} }
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sub2clash</title>
<link rel="stylesheet" href="./src/index.css" />
<script type="module" src="/src/app.ts"></script>
</head>
<body>
<sub2clash-app>
</sub2clash-app>
</body>
</html>
+2028
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "sub2clash-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"axios": "^1.12.2",
"daisyui": "^5.3.7",
"lit": "^3.3.1",
"tailwindcss": "^4.1.14"
},
"devDependencies": {
"typescript": "~5.9.3",
"vite": "^7.2.6"
}
}
+680
View File
@@ -0,0 +1,680 @@
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import globalStyles from "./index.css?inline";
import { type Config, type Rule, type RuleProvider } from "./interface.js";
import axios, { AxiosError } from "axios";
import { base64EncodeUnicode, base64decodeUnicode } from "./utils.js";
import "./components/rule-provider-input.js";
import "./components/rule-input.js";
import "./components/rename-input.js";
import "./components/short-link-input-group.js";
@customElement("sub2clash-app")
export class Sub2clashApp extends LitElement {
static styles = [unsafeCSS(globalStyles)];
private _config: Config = {
clashType: 2,
subscriptions: [],
proxies: [],
refresh: false,
autoTest: false,
lazy: false,
ignoreCountryGroup: false,
useUDP: false,
template: "",
sort: "nameasc",
remove: "",
nodeList: false,
ruleProviders: [],
replace: undefined,
rules: [],
};
@state()
set config(value: Config) {
console.log(JSON.stringify(value));
if (
(value.subscriptions == null || value.subscriptions.length == 0) &&
(value.proxies == null || value.proxies.length == 0)
) {
this.configUrl = "";
return;
}
const oldValue = this._config;
this.configUrl = `${
window.location.origin
}${window.location.pathname.replace(
/\/$/,
""
)}/convert/${base64EncodeUnicode(JSON.stringify(value))
.replace(/\+/g, "-")
.replace(/\//g, "_")}`;
this._config = value;
this.requestUpdate("config", oldValue);
}
get config(): Config {
return this._config;
}
@state({
hasChanged(value: boolean) {
localStorage.setItem("theme", value ? "dark" : "light");
document
.querySelector("html")
?.setAttribute("data-theme", value ? "dark" : "light");
return true;
},
})
darkTheme: boolean = this.initTheme();
initTheme(): boolean {
const savedTheme = localStorage.getItem("theme");
if (savedTheme != null) {
return savedTheme === "dark" ? true : false;
}
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
return prefersDark;
}
@state()
reverseUrl: string = "";
@state()
dialogMessage: string = "";
@state()
dialogTitle: string = "";
@query("dialog#my_modal")
dialog!: HTMLDialogElement;
showDialog(title: string, message: string): void {
if (title.trim() === "") {
title = "警告";
}
this.dialogTitle = title;
this.dialogMessage = message;
this.dialog.showModal();
}
@state()
configUrl: string = "";
@state()
shortLinkID: string = "";
@state()
shortLinkPasswd: string = "";
async copyToClipboard(content: string, e: HTMLButtonElement) {
try {
await navigator.clipboard.writeText(content);
let text = e.textContent;
e.addEventListener("mouseout", function () {
e.textContent = text;
});
e.textContent = "复制成功";
} catch (err) {
console.error("复制到剪贴板失败:", err);
}
}
generateShortLink() {
if (this.configUrl === "") {
this.showDialog("", "还未填写配置");
return;
}
axios
.post(
"./short",
{
config: this.config,
password: this.shortLinkPasswd,
id: this.shortLinkID,
},
{
headers: {
"Content-Type": "application/json",
},
}
)
.then((response) => {
// 设置返回的短链ID和密码
this.shortLinkID = response.data.id;
this.shortLinkPasswd = response.data.password;
this.showDialog("成功", "生成短链成功");
})
.catch((error) => {
if (error.response && error.response.data) {
this.showDialog("", "生成短链失败:" + error.response.data);
} else {
this.showDialog("", "生成短链失败");
}
});
}
updateShortLink() {
if (this.shortLinkID.trim() === "") {
this.showDialog("", "请输入ID");
return;
}
if (this.shortLinkPasswd.trim() === "") {
this.showDialog("", "请输入密码");
return;
}
if (this.configUrl === "") {
this.showDialog("", "还未填写配置");
return;
}
axios
.put(
"./short",
{
id: this.shortLinkID,
config: this.config,
password: this.shortLinkPasswd,
},
{
headers: {
"Content-Type": "application/json",
},
}
)
.then(() => {
this.showDialog("成功", "更新短链成功");
})
.catch((error) => {
if (error.response && error.response.status === 401) {
this.showDialog("", "短链不存在或密码错误");
} else if (error.response && error.response.data) {
this.showDialog("", "更新短链失败:" + error.response.data);
} else {
this.showDialog("", "更新短链失败");
}
});
}
deleteShortLink() {
if (this.shortLinkID.trim() === "") {
this.showDialog("", "请输入ID");
return;
}
if (this.shortLinkPasswd.trim() === "") {
this.showDialog("", "请输入密码");
return;
}
const params = new URLSearchParams();
params.append("password", this.shortLinkPasswd);
axios
.delete(`./short/${this.shortLinkID}?${params.toString()}`, {
headers: {
"Content-Type": "application/json",
},
})
.then(() => {
this.showDialog("成功", "删除短链成功");
})
.catch((error) => {
if (error.response && error.response.status === 401) {
this.showDialog("", "短链不存在或密码错误");
} else if (error.response && error.response.data) {
this.showDialog("", "删除短链失败:" + error.response.data);
} else {
this.showDialog("", "删除短链失败");
}
});
}
getRawConfigFromShortLink() {
const s = this.reverseUrl.split("/s/");
if (s.length != 2) {
this.showDialog("", "解析失败");
return;
}
axios
.get(`./short/${s[1]}`)
.then((resp) => {
this.config = resp.data;
const parsedUrl = new URL(this.reverseUrl);
this.shortLinkID = parsedUrl.pathname.split("/").filter(Boolean).pop() ?? "";
this.shortLinkPasswd = parsedUrl.searchParams.get("password") ?? "";
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
this.showDialog("", "短链不存在或密码错误");
} else if (err.response && err.response.data) {
this.showDialog("", "获取配置失败:" + err.response.data);
} else {
this.showDialog("", "获取配置失败");
}
});
}
parseConfig() {
if (this.reverseUrl.trim() === "") {
this.showDialog("", "无法解析,链接为空");
}
if (this.reverseUrl.indexOf("/s/") != -1) {
this.getRawConfigFromShortLink();
return;
}
let url = new URL(this.reverseUrl);
const pathSections = url.pathname.split("/");
if (pathSections.length < 2) {
this.showDialog("", "无法解析,链接格式错误");
}
if (pathSections[pathSections.length - 2] == "convert") {
let base64Data = pathSections[pathSections.length - 1];
base64Data = base64Data.replace(/-/g, "+").replace(/_/g, "/");
try {
const configData = base64decodeUnicode(base64Data);
this.config = JSON.parse(configData) as Config;
} catch (e: any) {
this.showDialog("", "无法解析 Base64,配置格式错误");
return;
}
} else {
this.showDialog("", "无法解析,链接格式错误");
}
}
render() {
return html`
<dialog id="my_modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">${this.dialogTitle}</h3>
<p class="py-4">${this.dialogMessage}</p>
<div class="modal-action">
<form method="dialog">
<button class="btn">关闭</button>
</form>
</div>
</div>
</dialog>
<div class="max-w-4xl mx-auto p-4 flex flex-col items-center">
<form class="w-full max-w-2xl bg-base-100">
<fieldset class="fieldset mb-6">
<div class="flex flex-row justify-between items-center my-6">
<legend
class="fieldset-legend text-2xl font-semibold inline-block m-0 p-0">
sub2clash
</legend>
<label class="swap swap-rotate h-7 w-7">
<!-- this hidden checkbox controls the state -->
<input
type="checkbox"
class="theme-controller"
.checked="${!this.darkTheme}"
@change="${() => (this.darkTheme = !this.darkTheme)}" />
<!-- sun icon -->
<svg
class="swap-off h-7 w-7 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg
class="swap-on h-7 w-7 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>
<!-- Input URL -->
<div class="form-control mb-5">
<label class="label mb-1 pl-1">解析链接</label>
<div class="join w-full">
<input
class="input input-bordered w-full join-item"
type="text"
@change="${(e: Event) => {
this.reverseUrl = (e.target as HTMLInputElement).value;
}}"
placeholder="通过生成的链接重新填写下方设置" />
<button
class="btn btn-primary join-item"
@click="${this.parseConfig}"
type="button">
解析
</button>
</div>
</div>
<!-- API Endpoint -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="endpoint">客户端类型</label>
<select
class="select select-bordered w-full"
name="endpoint"
.value="${this.config.clashType == 1 ? "1" : "2"}"
@change="${(e: Event) => {
this.config = {
...this.config,
clashType: Number((e.target as HTMLInputElement).value),
};
}}">
<option value="1">Clash</option>
<option value="2" selected>Clash.Meta</option>
</select>
</div>
<!-- Template -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="template">模板链接</label>
<input
class="input input-bordered w-full"
name="template"
placeholder="输入模板链接"
type="text"
.value="${this.config.template ?? ""}"
@change="${(e: Event) => {
this.config = {
...this.config,
template: (e.target as HTMLInputElement).value,
};
}}" />
</div>
<!-- Subscription Link -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="sub">订阅链接</label>
<div>
<textarea
class="textarea textarea-bordered h-24 w-full"
name="sub"
placeholder="每行输入一个订阅链接"
.value="${this.config.subscriptions
? this.config.subscriptions.join("\n")
: ""}"
@change="${(e: Event) => {
this.config = {
...this.config,
subscriptions: (e.target as HTMLInputElement).value
.split("\n")
.filter((e) => e.trim() !== ""),
};
}}"></textarea>
</div>
</div>
<!-- Proxy Link -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="proxy">节点分享链接</label>
<div>
<textarea
class="textarea textarea-bordered h-24 w-full"
name="proxy"
placeholder="每行输入一个节点分享链接"
.value="${this.config.proxies
? this.config.proxies.join("\n")
: ""}"
@change="${(e: Event) => {
this.config = {
...this.config,
proxies: (e.target as HTMLInputElement).value
.split("\n")
.filter((e) => e.trim() !== ""),
};
}}"></textarea>
</div>
</div>
<!-- User Agent -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="user-agent">UA 标识</label>
<div>
<textarea
class="textarea textarea-bordered h-20 w-full"
name="user-agent"
placeholder="用于获取订阅的 http 请求中的 User-Agent 标识"
.value="${this.config.userAgent ?? ""}"
@change="${(e: Event) => {
this.config = {
...this.config,
userAgent: (e.target as HTMLInputElement).value,
};
}}"></textarea>
</div>
</div>
<!-- Sort -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="sort">
国家策略组排序规则
</label>
<select
class="select select-bordered w-full"
name="sort"
.value="${this.config.sort ?? "nameasc"}"
@change="${(e: Event) => {
this.config = {
...this.config,
sort: (e.target as HTMLInputElement).value,
};
}}">
<option value="nameasc">名称(升序)</option>
<option value="namedesc">名称(降序)</option>
<option value="sizeasc">节点数量(升序)</option>
<option value="sizedesc">节点数量(降序)</option>
</select>
</div>
<!-- Remove -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1" for="remove">
<span class="label-text">排除节点</span>
</label>
<input
class="input input-bordered w-full"
type="text"
name="remove"
placeholder="正则表达式"
.value="${this.config.remove ?? ""}"
@change="${(e: Event) => {
this.config = {
...this.config,
remove: (e.target as HTMLInputElement).value,
};
}}" />
</div>
<!-- Checkboxes -->
<div class="form-control mb-3">
<label class="label cursor-pointer">
<input
type="checkbox"
name="refresh"
class="checkbox"
.checked="${this.config.refresh ?? false}"
@change="${(e: Event) => {
this.config = {
...this.config,
refresh: (e.target as HTMLInputElement).checked,
};
}}" />
强制重新获取订阅
</label>
</div>
<div class="form-control mb-3">
<label class="label cursor-pointer">
<input
type="checkbox"
name="nodeList"
class="checkbox"
.checked="${this.config.nodeList ?? false}"
@change="${(e: Event) => {
this.config = {
...this.config,
nodeList: (e.target as HTMLInputElement).checked,
};
}}" />
输出为 Node List
</label>
</div>
<div class="form-control mb-3">
<label class="label cursor-pointer">
<input
type="checkbox"
name="autoTest"
class="checkbox"
.checked="${this.config.autoTest ?? false}"
@change="${(e: Event) => {
this.config = {
...this.config,
autoTest: (e.target as HTMLInputElement).checked,
};
}}" />
国家策略组自动测速
</label>
</div>
<div class="form-control mb-3">
<label class="label cursor-pointer">
<input
type="checkbox"
name="lazy"
class="checkbox"
.checked="${this.config.lazy ?? false}"
@change="${(e: Event) => {
this.config = {
...this.config,
lazy: (e.target as HTMLInputElement).checked,
};
}}" />
自动测速启用 lazy 模式
</label>
</div>
<div class="form-control mb-3">
<label class="label cursor-pointer">
<input
type="checkbox"
name="igcg"
class="checkbox"
.checked="${this.config.ignoreCountryGroup ?? false}"
@change="${(e: Event) => {
this.config = {
...this.config,
ignoreCountryGroup: (e.target as HTMLInputElement)
.checked,
};
}}" />
不输出国家策略组
</label>
</div>
<div class="form-control mb-5">
<label class="label cursor-pointer">
<input
type="checkbox"
name="useUDP"
class="checkbox"
.checked="${this.config.useUDP ?? false}"
@change="${(e: Event) => {
this.config = {
...this.config,
useUDP: (e.target as HTMLInputElement).checked,
};
}}" />
使用 UDP
</label>
</div>
<rule-provider-input
@change="${(e: CustomEvent<Array<RuleProvider>>) => {
this.config = {
...this.config,
ruleProviders: e.detail,
};
}}"></rule-provider-input>
<rule-input
@change="${(e: CustomEvent<Array<Rule>>) => {
this.config = {
...this.config,
rules: e.detail,
};
}}"></rule-input>
<rename-input
@change="${(e: CustomEvent<{ [key: string]: string }>) => {
this.config = {
...this.config,
replace: e.detail,
};
}}"></rename-input>
</fieldset>
<fieldset class="fieldset mb-8">
<legend class="fieldset-legend text-2xl font-semibold mb-4">
输出配置
</legend>
<!-- Display the API Link -->
<div class="form-control mb-5">
<div class="join w-full mb-2">
<input
class="input input-bordered w-full join-item cursor-not-allowed"
type="text"
placeholder="链接"
.value="${this.configUrl}"
readonly />
<button
class="btn btn-primary join-item"
@click="${(e: Event) => {
this.copyToClipboard(
this.configUrl,
e.target as HTMLButtonElement
);
}}"
type="button">
复制链接
</button>
</div>
</div>
<short-link-input-group
.id="${this.shortLinkID}"
.passwd="${this.shortLinkPasswd}"
@id-change="${(e: Event) => {
this.shortLinkID = (e.target as HTMLInputElement).value;
}}"
@passwd-change="${(e: Event) => {
this.shortLinkPasswd = (e.target as HTMLInputElement).value;
}}"
@generate-btn-click="${this.generateShortLink}"
@update-btn-click="${this.updateShortLink}"
@delete-btn-click="${this.deleteShortLink}">
</short-link-input-group>
</fieldset>
</form>
</div>
<footer class="footer footer-horizontal footer-center mb-8">
<aside>
<p>
Powered by
<a class="link" href="https://github.com/bestnite/sub2clash"
>sub2clash</a
>
</p>
<p>Version: ${import.meta.env.VITE_APP_VERSION ?? "dev"}</p>
</aside>
</footer>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"sub2clash-app": Sub2clashApp;
}
}
@@ -0,0 +1,101 @@
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, state } from "lit/decorators.js";
import globalStyles from "../index.css?inline";
import type { Rename } from "../interface";
@customElement("rename-input")
export class RenameInput extends LitElement {
static styles = [unsafeCSS(globalStyles)];
private _replaceArray: Array<Rename> = [];
@state()
set replaceArray(value: Array<Rename>) {
this._replaceArray = value;
let updatedReplaceMap: { [key: string]: string } = {};
value.forEach((e) => {
updatedReplaceMap[e.old] = e.new;
});
this.dispatchEvent(
new CustomEvent("change", {
detail: updatedReplaceMap,
})
);
}
get replaceArray(): Array<Rename> {
return this._replaceArray;
}
render() {
return html`<!-- Rename -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1">
<span class="label-text">节点名称替换</span>
<button
class="btn btn-primary btn-xs"
type="button"
@click="${() => {
let updatedReplaceArray = [...this.replaceArray];
updatedReplaceArray.push({ old: "", new: "" });
this.replaceArray = updatedReplaceArray;
}}">
+
</button>
</label>
</div>
<div class="mb-3">
${this.replaceArray.map((_, i) => this.RenameTemplate(i))}
</div>`;
}
RenameTemplate(index: number) {
const replaceItem = this.replaceArray[index];
return html`<div class="join mb-1">
<input
class="input join-item"
placeholder="旧名称 (正则表达式)"
.value="${replaceItem?.old ?? ""}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedReplaceArray = [...this.replaceArray];
updatedReplaceArray[index] = {
...updatedReplaceArray[index],
old: target.value,
};
this.replaceArray = updatedReplaceArray;
}}" />
<input
class="input join-item"
placeholder="新名称"
.value="${replaceItem?.new ?? ""}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedReplaceArray = [...this.replaceArray];
updatedReplaceArray[index] = {
...updatedReplaceArray[index],
new: target.value,
};
this.replaceArray = updatedReplaceArray;
}}" />
<button
class="btn join-item bg-error"
type="button"
@click="${() => {
let updatedReplaceArray = this.replaceArray.filter(
(_, i) => i !== index
);
this.replaceArray = updatedReplaceArray;
}}">
删除
</button>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"rename-input": RenameInput;
}
}
@@ -0,0 +1,92 @@
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, state } from "lit/decorators.js";
import type { Rule } from "../interface";
import globalStyles from "../index.css?inline";
@customElement("rule-input")
export class RuleInput extends LitElement {
static styles = [unsafeCSS(globalStyles)];
_rules: Array<Rule> = [];
@state()
set rules(value: Array<Rule>) {
this.dispatchEvent(
new CustomEvent("change", {
detail: value,
})
);
this._rules = value;
}
get rules() {
return this._rules;
}
render() {
return html`<!-- Rule -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1">
<span class="label-text">规则</span>
<button
class="btn btn-primary btn-xs"
type="button"
@click="${() => {
let updatedRules = this.rules ? [...this.rules] : [];
updatedRules?.push({
rule: "",
prepend: false,
});
this.rules = updatedRules;
}}">
+
</button>
</label>
</div>
<div class="mb-3">
${this.rules?.map((_, i) => this.RuleTemplate(i))}
</div>`;
}
RuleTemplate(index: number) {
return html`<div class="join mb-1">
<input
class="input join-item"
placeholder="规则"
.value="${this.rules![index].rule}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRules = this.rules;
updatedRules![index].rule = target.value;
this.rules = updatedRules;
}}" />
<div class="tooltip" data-tip="是否置于规则列表最前">
<select
class="select join-item w-fit"
.value="${String(this.rules![index].prepend)}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRules = this.rules;
updatedRules![index].prepend = Boolean(target.value);
this.rules = updatedRules;
}}">
<option value="true">是</option>
<option value="false" selected>否</option>
</select>
</div>
<button
class="btn join-item bg-error"
type="button"
@click="${() => {
let updatedRules = this.rules?.filter((_, i) => i !== index);
this.rules = updatedRules;
}}">
删除
</button>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"rule-input": RuleInput;
}
}
@@ -0,0 +1,143 @@
import { LitElement, html, unsafeCSS } from "lit";
import { customElement, state } from "lit/decorators.js";
import type { RuleProvider } from "../interface";
import globalStyles from "../index.css?inline";
@customElement("rule-provider-input")
export class RuleProviderInput extends LitElement {
static styles = [unsafeCSS(globalStyles)];
_ruleProviders: Array<RuleProvider> = [];
@state()
set ruleProviders(value) {
this.dispatchEvent(
new CustomEvent("change", {
detail: value,
})
);
this._ruleProviders = value;
}
get ruleProviders() {
return this._ruleProviders;
}
RuleProviderTemplate(index: number) {
return html`
<div class="join mb-1">
<div class="tooltip" data-tip="不能重复">
<input
class="input join-item"
placeholder="名称"
.value="${this.ruleProviders![index].name}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRuleProviders = this.ruleProviders;
updatedRuleProviders![index].name = target.value;
this.ruleProviders = updatedRuleProviders;
}}" />
</div>
<div class="tooltip" data-tip="类型">
<select
class="select join-item w-fit"
.value="${this.ruleProviders![index].behavior}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRuleProviders = this.ruleProviders;
updatedRuleProviders![index].behavior = target.value;
this.ruleProviders = updatedRuleProviders;
}}">
<option value="classical" selected>classical</option>
<option value="domain">domain</option>
<option value="ipcidr">ipcidr</option>
</select>
</div>
<div>
<input
class="input join-item"
placeholder="Url"
.value="${this.ruleProviders![index].url}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRuleProviders = this.ruleProviders;
updatedRuleProviders![index].url = target.value;
this.ruleProviders = updatedRuleProviders;
}}" />
</div>
<input
class="input join-item"
placeholder="出站策略组"
.value="${this.ruleProviders![index].group}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRuleProviders = this.ruleProviders;
updatedRuleProviders![index].group = target.value;
this.ruleProviders = updatedRuleProviders;
}}" />
<div class="tooltip" data-tip="是否置于规则列表最前">
<select
class="select join-item w-fit"
.value="${String(this.ruleProviders![index].prepend)}"
@change="${(e: Event) => {
const target = e.target as HTMLInputElement;
let updatedRuleProviders = this.ruleProviders;
updatedRuleProviders![index].prepend = Boolean(target.value);
this.ruleProviders = updatedRuleProviders;
}}">
<option value="true">是</option>
<option value="false" selected>否</option>
</select>
</div>
<button
class="btn join-item bg-error"
type="button"
@click="${() => {
let updatedRuleProviders = this.ruleProviders?.filter(
(_, i) => i !== index
);
this.ruleProviders = updatedRuleProviders;
}}">
删除
</button>
</div>
`;
}
render() {
return html` <!-- Rule Provider -->
<div class="form-control mb-3">
<label class="label mb-1 pl-1">
<span class="label-text">Rule Provider</span>
<button
class="btn btn-primary btn-xs"
type="button"
@click="${() => {
let updatedRuleProviders = this.ruleProviders
? [...this.ruleProviders]
: [];
updatedRuleProviders.push({
behavior: "classical",
url: "",
name: "",
prepend: false,
group: "",
});
this.ruleProviders = updatedRuleProviders;
}}">
+
</button>
</label>
</div>
<div class="mb-3">
${this.ruleProviders?.map((_, i) => this.RuleProviderTemplate(i))}
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"rule-provider-input": RuleProviderInput;
}
}
@@ -0,0 +1,180 @@
import { html, LitElement, unsafeCSS } from "lit";
import { customElement, property } from "lit/decorators.js";
import globalStyles from "../index.css?inline";
@customElement("short-link-input-group")
export class ShortLinkInputGroup extends LitElement {
static styles = unsafeCSS(globalStyles);
@property()
id: string = "";
@property({ type: Number })
_screenSizeLevel: number = 0;
@property()
passwd: string = "";
connectedCallback() {
super.connectedCallback();
window.addEventListener("resize", this._checkScreenSize);
this._checkScreenSize(); // Initial check
}
disconnectedCallback() {
window.removeEventListener("resize", this._checkScreenSize);
super.disconnectedCallback();
}
_checkScreenSize = () => {
const width = window.innerWidth;
if (width < 365) {
this._screenSizeLevel = 0; // sm
} else if (width < 640) {
this._screenSizeLevel = 1; // md
} else {
this._screenSizeLevel = 2; // other
}
};
async copyToClipboard(content: string, e: HTMLButtonElement) {
try {
await navigator.clipboard.writeText(content);
let text = e.textContent;
e.addEventListener("mouseout", function () {
e.textContent = text;
});
e.textContent = "复制成功";
} catch (err) {
console.error("复制到剪贴板失败:", err);
}
}
idInputTemplate() {
return html`<input
class="input input-bordered w-1/2 join-item"
type="text"
placeholder="ID(可选)"
.value="${this.id}"
@change="${(e: Event) => {
this.id = (e.target as HTMLInputElement).value;
this.dispatchEvent(
new CustomEvent("id-change", {
detail: this.id,
})
);
}}" />`;
}
passwdInputTemplate() {
return html`<input
class="input input-bordered w-1/2 join-item"
type="text"
placeholder="密码"
.value="${this.passwd}"
@change="${(e: Event) => {
this.passwd = (e.target as HTMLInputElement).value;
this.dispatchEvent(
new CustomEvent("passwd-change", {
detail: this.passwd,
})
);
}}" />`;
}
generateBtnTemplate(extraClass: string = "") {
return html`<button
class="btn btn-primary join-item ${extraClass}"
type="button"
@click="${(e: Event) => {
this.dispatchEvent(
new CustomEvent("generate-btn-click", { detail: e })
);
}}">
生成短链
</button>`;
}
updateBtnTemplate(extraClass: string = "") {
return html`<button
class="btn btn-primary join-item ${extraClass}"
@click="${(e: Event) => {
this.dispatchEvent(new CustomEvent("update-btn-click", { detail: e }));
}}"
type="button">
更新短链
</button>`;
}
deleteBtnTemplate(extraClass: string = "") {
return html`<button
class="btn btn-primary join-item ${extraClass}"
@click="${(e: Event) => {
this.dispatchEvent(new CustomEvent("delete-btn-click", { detail: e }));
}}"
type="button">
删除短链
</button>`;
}
copyBtnTemplate(extraClass: string = "") {
return html`<button
class="btn btn-primary join-item ${extraClass}"
type="button"
@click="${(e: Event) => {
this.copyToClipboard(
`${window.location.origin}${window.location.pathname}s/${this.id}?password=${this.passwd}`,
e.target as HTMLButtonElement
);
}}">
复制短链
</button>`;
}
render() {
const sm = html`<div class="form-control mb-2">
<div class="join w-full mb-1">
${this.idInputTemplate()} ${this.passwdInputTemplate()}
</div>
<div class="join w-full mb-1">
${this.generateBtnTemplate("w-1/2")} ${this.updateBtnTemplate("w-1/2")}
</div>
<div class="join w-full">
${this.deleteBtnTemplate("w-1/2")} ${this.copyBtnTemplate("w-1/2")}
</div>
</div>`;
const md = html`<div class="form-control mb-2">
<div class="join w-full mb-1">
${this.idInputTemplate()} ${this.passwdInputTemplate()}
</div>
<div class="join w-full">
${this.generateBtnTemplate("w-1/4")} ${this.updateBtnTemplate("w-1/4")}
${this.deleteBtnTemplate("w-1/4")} ${this.copyBtnTemplate("w-1/4")}
</div>
</div>`;
const other = html`<div class="form-control mb-2">
<div class="join w-full">
${this.idInputTemplate()} ${this.passwdInputTemplate()}
${this.generateBtnTemplate()} ${this.updateBtnTemplate()}
${this.deleteBtnTemplate()} ${this.copyBtnTemplate()}
</div>
</div>`;
switch (this._screenSizeLevel) {
case 0:
return sm;
case 1:
return md;
default:
return other;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"short-link-input-group": ShortLinkInputGroup;
}
}
+72
View File
@@ -0,0 +1,72 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui/theme" {
name: "light";
default: false;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(98% 0.001 106.423);
--color-base-200: oklch(97% 0.001 106.424);
--color-base-300: oklch(92% 0.003 48.717);
--color-base-content: oklch(21% 0.006 56.043);
--color-primary: oklch(54% 0.281 293.009);
--color-primary-content: oklch(96% 0.016 293.756);
--color-secondary: oklch(57% 0.245 27.325);
--color-secondary-content: oklch(97% 0.013 17.38);
--color-accent: oklch(59% 0.249 0.584);
--color-accent-content: oklch(97% 0.014 343.198);
--color-neutral: oklch(14% 0.004 49.25);
--color-neutral-content: oklch(98% 0.001 106.423);
--color-info: oklch(78% 0.154 211.53);
--color-info-content: oklch(30% 0.056 229.695);
--color-success: oklch(79% 0.209 151.711);
--color-success-content: oklch(26% 0.065 152.934);
--color-warning: oklch(82% 0.189 84.429);
--color-warning-content: oklch(27% 0.077 45.635);
--color-error: oklch(71% 0.194 13.428);
--color-error-content: oklch(27% 0.105 12.094);
--radius-selector: 1rem;
--radius-field: 1rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1.5px;
--depth: 0;
--noise: 0;
}
@plugin "daisyui/theme" {
name: "dark";
default: true;
prefersdark: true;
color-scheme: "dark";
--color-base-100: oklch(21% 0.006 285.885);
--color-base-200: oklch(21% 0.006 285.885);
--color-base-300: oklch(27% 0.006 286.033);
--color-base-content: oklch(96% 0.001 286.375);
--color-primary: oklch(55% 0.288 302.321);
--color-primary-content: oklch(97% 0.014 308.299);
--color-secondary: oklch(44% 0.03 256.802);
--color-secondary-content: oklch(98% 0.002 247.839);
--color-accent: oklch(59% 0.249 0.584);
--color-accent-content: oklch(97% 0.014 343.198);
--color-neutral: oklch(37% 0.013 285.805);
--color-neutral-content: oklch(98% 0 0);
--color-info: oklch(54% 0.245 262.881);
--color-info-content: oklch(97% 0.014 254.604);
--color-success: oklch(64% 0.2 131.684);
--color-success-content: oklch(98% 0.031 120.757);
--color-warning: oklch(66% 0.179 58.318);
--color-warning-content: oklch(98% 0.022 95.277);
--color-error: oklch(58% 0.253 17.585);
--color-error-content: oklch(96% 0.015 12.422);
--radius-selector: 1rem;
--radius-field: 1rem;
--radius-box: 1rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 0;
--noise: 1;
}
+36
View File
@@ -0,0 +1,36 @@
export interface RuleProvider {
behavior: string;
url: string;
group: string;
prepend: boolean;
name: string;
}
export interface Rule {
rule: string;
prepend: boolean;
}
export interface Rename {
old: string;
new: string;
}
export interface Config {
clashType: number;
subscriptions?: string[];
proxies?: string[];
userAgent?: string;
refresh?: boolean;
autoTest?: boolean;
lazy?: boolean;
nodeList?: boolean;
ignoreCountryGroup?: boolean;
useUDP?: boolean;
template?: string;
ruleProviders?: RuleProvider[];
rules?: Rule[];
sort?: string;
remove?: string;
replace?: { [key: string]: string };
}
+15
View File
@@ -0,0 +1,15 @@
export function base64EncodeUnicode(str: string) {
const bytes = new TextEncoder().encode(str);
let binary = "";
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary);
}
export function base64decodeUnicode(str: string) {
const binaryString = atob(str);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
}
+31
View File
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"types": [
"vite/client"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src"
]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});
+58
View File
@@ -0,0 +1,58 @@
package handler
import (
_ "embed"
"net/http"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/config"
"github.com/bestnite/sub2clash/model"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
func ConvertHandler() func(c *gin.Context) {
return func(c *gin.Context) {
query, err := model.ParseConvertQuery(c)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
template := ""
switch query.ClashType {
case model.Clash:
template = config.GlobalConfig.ClashTemplate
case model.ClashMeta:
template = config.GlobalConfig.MetaTemplate
}
sub, err := common.BuildSub(query.ClashType, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
if len(query.Subs) == 1 {
userInfoHeader, err := common.FetchSubscriptionUserInfo(query.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
if err == nil {
c.Header("subscription-userinfo", userInfoHeader)
}
}
if query.NodeListMode {
marshal, err := sub.MarshalNodeListYAML()
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}
}
+250
View File
@@ -0,0 +1,250 @@
package handler
import (
"encoding/json"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/bestnite/sub2clash/common"
"github.com/bestnite/sub2clash/common/database"
"github.com/bestnite/sub2clash/config"
"github.com/bestnite/sub2clash/model"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
type shortLinkGenRequset struct {
Config model.ConvertConfig `form:"config" binding:"required"`
Password string `form:"password"`
ID string `form:"id"`
}
type shortLinkUpdateRequest struct {
Config model.ConvertConfig `form:"config" binding:"required"`
Password string `form:"password" binding:"required"`
ID string `form:"id" binding:"required"`
}
var DB *database.Database
func init() {
var err error
DB, err = database.ConnectDB()
if err != nil {
log.Printf("failed to connect to database: %v", err)
os.Exit(1)
}
}
func GenerateLinkHandler(c *gin.Context) {
var params shortLinkGenRequset
if err := c.ShouldBind(&params); err != nil {
c.String(http.StatusBadRequest, "参数错误: "+err.Error())
return
}
var id string
var password string
var err error
if params.ID != "" {
// 检查自定义ID是否已存在
exists, err := DB.CheckShortLinkIDExists(params.ID)
if err != nil {
c.String(http.StatusInternalServerError, "数据库错误")
return
}
if exists {
c.String(http.StatusBadRequest, "短链已存在")
return
}
id = params.ID
password = params.Password
} else {
// 自动生成短链ID和密码
id, err = generateUniqueHash(config.GlobalConfig.ShortLinkLength)
if err != nil {
c.String(http.StatusInternalServerError, "生成短链失败")
return
}
if params.Password == "" {
password = common.RandomString(8) // 生成8位随机密码
} else {
password = params.Password
}
}
shortLink := model.ShortLink{
ID: id,
Config: params.Config,
Password: password,
}
if err := DB.CreateShortLink(&shortLink); err != nil {
c.String(http.StatusInternalServerError, "数据库错误")
return
}
// 返回生成的短链ID和密码
response := map[string]string{
"id": id,
"password": password,
}
c.JSON(http.StatusOK, response)
}
func generateUniqueHash(length int) (string, error) {
for {
hash := common.RandomString(length)
exists, err := DB.CheckShortLinkIDExists(hash)
if err != nil {
return "", err
}
if !exists {
return hash, nil
}
}
}
func UpdateLinkHandler(c *gin.Context) {
var params shortLinkUpdateRequest
if err := c.ShouldBindJSON(&params); err != nil {
c.String(http.StatusBadRequest, "参数错误: "+err.Error())
return
}
// 先获取原有的短链
existingLink, err := DB.FindShortLinkByID(params.ID)
if err != nil {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
// 验证密码
if existingLink.Password != params.Password {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
jsonData, err := json.Marshal(params.Config)
if err != nil {
c.String(http.StatusBadRequest, "配置格式错误")
return
}
if err := DB.UpdataShortLink(params.ID, "config", jsonData); err != nil {
c.String(http.StatusInternalServerError, "数据库错误")
return
}
c.String(http.StatusOK, "短链更新成功")
}
func GetRawConfHandler(c *gin.Context) {
id := c.Param("id")
password := c.Query("password")
if strings.TrimSpace(id) == "" {
c.String(http.StatusBadRequest, "参数错误")
return
}
shortLink, err := DB.FindShortLinkByID(id)
if err != nil {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
if shortLink.Password != "" && shortLink.Password != password {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
err = DB.UpdataShortLink(shortLink.ID, "last_request_time", time.Now().Unix())
if err != nil {
c.String(http.StatusInternalServerError, "数据库错误")
return
}
template := ""
switch shortLink.Config.ClashType {
case model.Clash:
template = config.GlobalConfig.ClashTemplate
case model.ClashMeta:
template = config.GlobalConfig.MetaTemplate
}
sub, err := common.BuildSub(shortLink.Config.ClashType, shortLink.Config, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
if len(shortLink.Config.Subs) == 1 {
userInfoHeader, err := common.FetchSubscriptionUserInfo(shortLink.Config.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
if err == nil {
c.Header("subscription-userinfo", userInfoHeader)
}
}
if shortLink.Config.NodeListMode {
marshal, err := sub.MarshalNodeListYAML()
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
}
func GetRawConfUriHandler(c *gin.Context) {
id := c.Param("id")
password := c.Query("password")
if strings.TrimSpace(id) == "" {
c.String(http.StatusBadRequest, "参数错误")
return
}
shortLink, err := DB.FindShortLinkByID(id)
if err != nil {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
if shortLink.Password != "" && shortLink.Password != password {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
c.JSON(http.StatusOK, shortLink.Config)
}
func DeleteShortLinkHandler(c *gin.Context) {
id := c.Param("id")
password := c.Query("password")
shortLink, err := DB.FindShortLinkByID(id)
if err != nil {
c.String(http.StatusBadRequest, "短链不存在或密码错误")
return
}
if shortLink.Password != password {
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
return
}
err = DB.DeleteShortLink(id)
if err != nil {
c.String(http.StatusInternalServerError, "删除失败", err)
}
}
@@ -1,11 +1,13 @@
package middleware package middleware
import ( import (
"strconv"
"time"
"github.com/bestnite/sub2clash/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
"strconv"
"sub2clash/logger"
"time"
) )
func ZapLogger() gin.HandlerFunc { func ZapLogger() gin.HandlerFunc {
+32
View File
@@ -0,0 +1,32 @@
package server
import (
"embed"
"net/http"
"github.com/bestnite/sub2clash/server/handler"
"github.com/bestnite/sub2clash/server/middleware"
"github.com/gin-gonic/gin"
)
//go:embed frontend/dist
var staticFiles embed.FS
func SetRoute(r *gin.Engine) {
r.GET("/convert/:config", middleware.ZapLogger(), handler.ConvertHandler())
r.GET("/s/:id", middleware.ZapLogger(), handler.GetRawConfHandler)
r.POST("/short", middleware.ZapLogger(), handler.GenerateLinkHandler)
r.PUT("/short", middleware.ZapLogger(), handler.UpdateLinkHandler)
r.GET("/short/:id", middleware.ZapLogger(), handler.GetRawConfUriHandler)
r.DELETE("/short/:id", middleware.ZapLogger(), handler.DeleteShortLinkHandler)
r.GET("/", func(c *gin.Context) {
c.FileFromFS("frontend/dist/", http.FS(staticFiles))
})
r.GET(
"/assets/*filepath", func(c *gin.Context) {
c.FileFromFS("frontend/dist/assets/"+c.Param("filepath"), http.FS(staticFiles))
},
)
}
+18 -2
View File
@@ -1,5 +1,4 @@
port: 7890 mixed-port: 7890
socks-port: 7891
allow-lan: true allow-lan: true
mode: Rule mode: Rule
log-level: info log-level: info
@@ -8,88 +7,104 @@ proxy-groups:
- name: 节点选择 - name: 节点选择
type: select type: select
proxies: proxies:
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 手动切换 - name: 手动切换
type: select type: select
proxies: proxies:
- <all>
- name: 电报消息 - name: 电报消息
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: OpenAi - name: OpenAi
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 油管视频 - name: 油管视频
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 巴哈姆特 - name: 巴哈姆特
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 哔哩哔哩 - name: 哔哩哔哩
type: select type: select
proxies: proxies:
- <countries>
- 全球直连 - 全球直连
- name: 国外媒体 - name: 国外媒体
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 国内媒体 - name: 国内媒体
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 手动切换 - 手动切换
- name: 谷歌FCM - name: 谷歌FCM
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 微软云盘 - name: 微软云盘
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 微软服务 - name: 微软服务
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 苹果服务 - name: 苹果服务
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 游戏平台 - name: 游戏平台
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- 手动切换 - 手动切换
- name: 网易音乐 - name: 网易音乐
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- name: 全球直连 - name: 全球直连
type: select type: select
proxies: proxies:
- <countries>
- DIRECT - DIRECT
- 节点选择 - 节点选择
- name: 广告拦截 - name: 广告拦截
@@ -105,6 +120,7 @@ proxy-groups:
- name: 漏网之鱼 - name: 漏网之鱼
type: select type: select
proxies: proxies:
- <countries>
- 节点选择 - 节点选择
- DIRECT - DIRECT
- 手动切换 - 手动切换
+77 -13
View File
@@ -1,5 +1,4 @@
port: 7890 mixed-port: 7890
socks-port: 7891
allow-lan: true allow-lan: true
mode: Rule mode: Rule
log-level: info log-level: info
@@ -8,34 +7,90 @@ proxy-groups:
- name: 节点选择 - name: 节点选择
type: select type: select
proxies: proxies:
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 手动切换 - name: 手动切换
type: select type: select
proxies: proxies:
- name: 游戏平台 - <all>
- name: 游戏平台(中国)
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 游戏平台(全球)
type: select
proxies:
- 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 巴哈姆特 - name: 巴哈姆特
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 哔哩哔哩 - name: 哔哩哔哩
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
- name: 全球直连 - name: Telegram
type: select type: select
proxies: proxies:
- DIRECT
- 节点选择 - 节点选择
- <countries>
- 手动切换
- DIRECT
- name: OpenAI
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Youtube
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Microsoft
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Onedrive
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Apple
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: Netflix
type: select
proxies:
- 节点选择
- <countries>
- 手动切换
- DIRECT
- name: 广告拦截 - name: 广告拦截
type: select type: select
proxies: proxies:
@@ -45,18 +100,27 @@ proxy-groups:
type: select type: select
proxies: proxies:
- 节点选择 - 节点选择
- <countries>
- 手动切换 - 手动切换
- DIRECT - DIRECT
rules: rules:
- GEOSITE,private,全球直连 - GEOSITE,private,DIRECT,no-resolve
- GEOIP,private,全球直连 - GEOIP,private,DIRECT
- GEOSITE,biliintl,哔哩哔哩 - GEOSITE,category-ads-all,广告拦截
- GEOSITE,microsoft,Microsoft
- GEOSITE,apple,Apple
- GEOSITE,netflix,Netflix
- GEOIP,netflix,Netflix
- GEOSITE,onedrive,Onedrive
- GEOSITE,youtube,Youtube
- GEOSITE,telegram,Telegram
- GEOIP,telegram,Telegram
- GEOSITE,openai,OpenAI
- GEOSITE,bilibili,哔哩哔哩 - GEOSITE,bilibili,哔哩哔哩
- GEOSITE,bahamut,巴哈姆特 - GEOSITE,bahamut,巴哈姆特
- GEOSITE,CN,全球直连 - GEOSITE,category-games@cn,游戏平台(中国)
- GEOIP,CN,全球直连 - GEOSITE,category-games,游戏平台(全球)
- GEOSITE,category-games,游戏平台
- GEOSITE,geolocation-!cn,节点选择 - GEOSITE,geolocation-!cn,节点选择
- GEOIP,ad,广告拦截 - GEOSITE,CN,DIRECT
- GEOSITE,category-ads-all,广告拦截 - GEOIP,CN,DIRECT
- MATCH,漏网之鱼 - MATCH,漏网之鱼
+219
View File
@@ -0,0 +1,219 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestAnytls_Basic_SimpleLink(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@127.0.0.1:8080#Anytls%20Proxy"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls Proxy",
Anytls: proxy.Anytls{
Server: "127.0.0.1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_WithSNI(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@proxy.example.com:443?sni=proxy.example.com#Anytls%20SNI"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls SNI",
Anytls: proxy.Anytls{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_WithInsecure(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com#Anytls%20Insecure"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls Insecure",
Anytls: proxy.Anytls{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_IPv6Address(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@[2001:db8::1]:8080#Anytls%20IPv6"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls IPv6",
Anytls: proxy.Anytls{
Server: "2001:db8::1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_ComplexPassword(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://ComplexPassword!%40%23%24@proxy.example.com:8443?sni=example.com&insecure=1#Anytls%20Full"
expected := proxy.Proxy{
Type: "anytls",
Name: "Anytls Full",
Anytls: proxy.Anytls{
Server: "proxy.example.com",
Port: 8443,
Password: "ComplexPassword!@#$",
SNI: "example.com",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_NoPassword(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://@127.0.0.1:8080#No%20Password"
expected := proxy.Proxy{
Type: "anytls",
Name: "No Password",
Anytls: proxy.Anytls{
Server: "127.0.0.1",
Port: 8080,
Password: "",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Basic_UsernameOnly(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://username@127.0.0.1:8080#Username%20Only"
expected := proxy.Proxy{
Type: "anytls",
Name: "Username Only",
Anytls: proxy.Anytls{
Server: "127.0.0.1",
Port: 8080,
Password: "username",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestAnytls_Error_MissingServer(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestAnytls_Error_MissingPort(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestAnytls_Error_InvalidPort(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anytls://password123@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestAnytls_Error_InvalidProtocol(t *testing.T) {
p := &parser.AnytlsParser{}
input := "anyssl://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+198
View File
@@ -0,0 +1,198 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestHysteria2_Basic_SimpleLink(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1:8080#Hysteria2%20Proxy"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Proxy",
Hysteria2: proxy.Hysteria2{
Server: "127.0.0.1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_AltPrefix(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hy2://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com#Hysteria2%20Alt"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Alt",
Hysteria2: proxy.Hysteria2{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_WithObfs(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1:8080?obfs=salamander&obfs-password=obfs123#Hysteria2%20Obfs"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Obfs",
Hysteria2: proxy.Hysteria2{
Server: "127.0.0.1",
Port: 8080,
Password: "password123",
Obfs: "salamander",
ObfsPassword: "obfs123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_IPv6Address(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@[2001:db8::1]:8080#Hysteria2%20IPv6"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 IPv6",
Hysteria2: proxy.Hysteria2{
Server: "2001:db8::1",
Port: 8080,
Password: "password123",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_FullConfig(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@proxy.example.com:443?insecure=1&sni=proxy.example.com&obfs=salamander&obfs-password=obfs123#Hysteria2%20Full"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "Hysteria2 Full",
Hysteria2: proxy.Hysteria2{
Server: "proxy.example.com",
Port: 443,
Password: "password123",
SNI: "proxy.example.com",
Obfs: "salamander",
ObfsPassword: "obfs123",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Basic_NoPassword(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://@127.0.0.1:8080#No%20Password"
expected := proxy.Proxy{
Type: "hysteria2",
Name: "No Password",
Hysteria2: proxy.Hysteria2{
Server: "127.0.0.1",
Port: 8080,
Password: "",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria2_Error_MissingServer(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria2_Error_MissingPort(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria2_Error_InvalidPort(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria2://password123@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria2_Error_InvalidProtocol(t *testing.T) {
p := &parser.Hysteria2Parser{}
input := "hysteria://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+183
View File
@@ -0,0 +1,183 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestHysteria_Basic_SimpleLink(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1:8080?protocol=udp&auth=password123&upmbps=100&downmbps=100#Hysteria%20Proxy"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Proxy",
Hysteria: proxy.Hysteria{
Server: "127.0.0.1",
Port: 8080,
Protocol: "udp",
Auth: "password123",
Up: "100",
Down: "100",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_WithAuthString(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://proxy.example.com:443?protocol=wechat-video&auth-str=myauth&upmbps=50&downmbps=200&insecure=true#Hysteria%20Auth"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Auth",
Hysteria: proxy.Hysteria{
Server: "proxy.example.com",
Port: 443,
Protocol: "wechat-video",
AuthString: "myauth",
Up: "50",
Down: "200",
SkipCertVerify: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_WithObfs(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1:8080?auth=password123&upmbps=100&downmbps=100&obfs=xplus&alpn=h3#Hysteria%20Obfs"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Obfs",
Hysteria: proxy.Hysteria{
Server: "127.0.0.1",
Port: 8080,
Auth: "password123",
Up: "100",
Down: "100",
Obfs: "xplus",
ALPN: []string{"h3"},
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_IPv6Address(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://[2001:db8::1]:8080?auth=password123&upmbps=100&downmbps=100#Hysteria%20IPv6"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria IPv6",
Hysteria: proxy.Hysteria{
Server: "2001:db8::1",
Port: 8080,
Auth: "password123",
Up: "100",
Down: "100",
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Basic_MultiALPN(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://proxy.example.com:443?auth=password123&upmbps=100&downmbps=100&alpn=h3,h2,http/1.1#Hysteria%20Multi%20ALPN"
expected := proxy.Proxy{
Type: "hysteria",
Name: "Hysteria Multi ALPN",
Hysteria: proxy.Hysteria{
Server: "proxy.example.com",
Port: 443,
Auth: "password123",
Up: "100",
Down: "100",
ALPN: []string{"h3", "h2", "http/1.1"},
SkipCertVerify: false,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestHysteria_Error_MissingServer(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://:8080?auth=password123"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria_Error_MissingPort(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1?auth=password123"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria_Error_InvalidPort(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria://127.0.0.1:99999?auth=password123"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestHysteria_Error_InvalidProtocol(t *testing.T) {
p := &parser.HysteriaParser{}
input := "hysteria2://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+184
View File
@@ -0,0 +1,184 @@
package test
import (
"errors"
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestShadowsocks_Basic_SimpleLink(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1:8080"
expected := proxy.Proxy{
Type: "ss",
Name: "127.0.0.1:8080",
ShadowSocks: proxy.ShadowSocks{
Server: "127.0.0.1",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Basic_IPv6Address(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@[2001:db8::1]:8080"
expected := proxy.Proxy{
Type: "ss",
Name: "2001:db8::1:8080",
ShadowSocks: proxy.ShadowSocks{
Server: "2001:db8::1",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Basic_WithRemark(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@proxy.example.com:8080#My%20SS%20Proxy"
expected := proxy.Proxy{
Type: "ss",
Name: "My SS Proxy",
ShadowSocks: proxy.ShadowSocks{
Server: "proxy.example.com",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Advanced_Base64FullEncoded(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmRAbG9jYWxob3N0OjgwODA=#Local%20SS"
expected := proxy.Proxy{
Type: "ss",
Name: "Local SS",
ShadowSocks: proxy.ShadowSocks{
Server: "localhost",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Advanced_PlainUserPassword(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://aes-256-gcm:password@192.168.1.1:8080"
expected := proxy.Proxy{
Type: "ss",
Name: "192.168.1.1:8080",
ShadowSocks: proxy.ShadowSocks{
Server: "192.168.1.1",
Port: 8080,
Cipher: "aes-256-gcm",
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocks_Advanced_ChaCha20Cipher(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://chacha20-poly1305:mypassword@server.com:443#ChaCha20"
expected := proxy.Proxy{
Type: "ss",
Name: "ChaCha20",
ShadowSocks: proxy.ShadowSocks{
Server: "server.com",
Port: 443,
Cipher: "chacha20-poly1305",
Password: "mypassword",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
// 错误处理测试
func TestShadowsocks_Error_MissingServer(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if !errors.Is(err, parser.ErrInvalidStruct) {
t.Errorf("Error is not expected: %v", err)
}
}
func TestShadowsocks_Error_MissingPort(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if !errors.Is(err, parser.ErrInvalidStruct) {
t.Errorf("Error is not expected: %v", err)
}
}
func TestShadowsocks_Error_InvalidProtocol(t *testing.T) {
p := &parser.ShadowsocksParser{}
input := "http://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if !errors.Is(err, parser.ErrInvalidPrefix) {
t.Errorf("Error is not expected: %v", err)
}
}
+112
View File
@@ -0,0 +1,112 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestShadowsocksR_Basic_SimpleLink(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://MTI3LjAuMC4xOjQ0MzpvcmlnaW46YWVzLTE5Mi1jZmI6cGxhaW46TVRJek1USXovP2dyb3VwPVpHVm1ZWFZzZEEmcmVtYXJrcz1TRUZJUVE"
expected := proxy.Proxy{
Type: "ssr",
Name: "HAHA",
ShadowSocksR: proxy.ShadowSocksR{
Server: "127.0.0.1",
Port: 443,
Cipher: "aes-192-cfb",
Password: "123123",
ObfsParam: "",
Obfs: "plain",
Protocol: "origin",
ProtocolParam: "",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocksR_Basic_WithParams(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://MTI3LjAuMC4xOjQ0MzpvcmlnaW46YWVzLTE5Mi1jZmI6dGxzMS4wX3Nlc3Npb25fYXV0aDpNVEl6TVRJei8/b2Jmc3BhcmFtPWIySm1jeTF3WVhKaGJXVjBaWEkmcHJvdG9wYXJhbT1jSEp2ZEc5allXd3RjR0Z5WVcxbGRHVnkmZ3JvdXA9WkdWbVlYVnNkQSZyZW1hcmtzPVNFRklRUQ"
expected := proxy.Proxy{
Type: "ssr",
Name: "HAHA",
ShadowSocksR: proxy.ShadowSocksR{
Server: "127.0.0.1",
Port: 443,
Cipher: "aes-192-cfb",
Password: "123123",
ObfsParam: "obfs-parameter",
Obfs: "tls1.0_session_auth",
Protocol: "origin",
ProtocolParam: "protocal-parameter",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocksR_Basic_IPv6Address(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://WzIwMDE6MGRiODo4NWEzOjAwMDA6MDAwMDo4YTJlOjAzNzA6NzMzNF06NDQzOm9yaWdpbjphZXMtMTkyLWNmYjpwbGFpbjpNVEl6TVRJei8/Z3JvdXA9WkdWbVlYVnNkQSZyZW1hcmtzPVNFRklRUQ"
expected := proxy.Proxy{
Type: "ssr",
Name: "HAHA",
ShadowSocksR: proxy.ShadowSocksR{
Server: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
Port: 443,
Cipher: "aes-192-cfb",
Password: "123123",
ObfsParam: "",
Obfs: "plain",
Protocol: "origin",
ProtocolParam: "",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestShadowsocksR_Error_InvalidBase64(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ssr://invalid_base64"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestShadowsocksR_Error_InvalidProtocol(t *testing.T) {
p := &parser.ShadowsocksRParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+168
View File
@@ -0,0 +1,168 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestSocks_Basic_SimpleLink(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:1080#SOCKS%20Proxy"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS Proxy",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
UserName: "user",
Password: "pass",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_NoAuth(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://127.0.0.1:1080#SOCKS%20No%20Auth"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS No Auth",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_IPv6Address(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@[2001:db8::1]:1080#SOCKS%20IPv6"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS IPv6",
Socks: proxy.Socks{
Server: "2001:db8::1",
Port: 1080,
UserName: "user",
Password: "pass",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_WithTLS(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:1080?tls=true&sni=example.com#SOCKS%20TLS"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS TLS",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
UserName: "user",
Password: "pass",
TLS: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Basic_WithUDP(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:1080?udp=true#SOCKS%20UDP"
expected := proxy.Proxy{
Type: "socks5",
Name: "SOCKS UDP",
Socks: proxy.Socks{
Server: "127.0.0.1",
Port: 1080,
UserName: "user",
Password: "pass",
UDP: true,
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestSocks_Error_MissingServer(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@:1080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestSocks_Error_MissingPort(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestSocks_Error_InvalidPort(t *testing.T) {
p := &parser.SocksParser{}
input := "socks://user:pass@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestSocks_Error_InvalidProtocol(t *testing.T) {
p := &parser.SocksParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+182
View File
@@ -0,0 +1,182 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestTrojan_Basic_SimpleLink(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443#Trojan%20Proxy"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan Proxy",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithTLS(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?security=tls&sni=example.com&alpn=h2,http/1.1#Trojan%20TLS"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan TLS",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
ALPN: []string{"h2", "http/1.1"},
SNI: "example.com",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithReality(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?security=reality&sni=example.com&pbk=publickey123&sid=shortid123&fp=chrome#Trojan%20Reality"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan Reality",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
SNI: "example.com",
RealityOpts: proxy.RealityOptions{
PublicKey: "publickey123",
ShortID: "shortid123",
},
Fingerprint: "chrome",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithWebSocket(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?type=ws&path=/ws&host=example.com#Trojan%20WS"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan WS",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/ws",
Headers: map[string]string{
"Host": "example.com",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Basic_WithGrpc(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:443?type=grpc&serviceName=grpc_service#Trojan%20gRPC"
expected := proxy.Proxy{
Type: "trojan",
Name: "Trojan gRPC",
Trojan: proxy.Trojan{
Server: "127.0.0.1",
Port: 443,
Password: "password",
Network: "grpc",
GrpcOpts: proxy.GrpcOptions{
GrpcServiceName: "grpc_service",
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestTrojan_Error_MissingServer(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@:443"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestTrojan_Error_MissingPort(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestTrojan_Error_InvalidPort(t *testing.T) {
p := &parser.TrojanParser{}
input := "trojan://password@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestTrojan_Error_InvalidProtocol(t *testing.T) {
p := &parser.TrojanParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+24
View File
@@ -0,0 +1,24 @@
package test
import (
"reflect"
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"gopkg.in/yaml.v3"
)
func validateResult(t *testing.T, expected proxy.Proxy, result proxy.Proxy) {
t.Helper()
if result.Type != expected.Type {
t.Errorf("Type mismatch: expected %s, got %s", expected.Type, result.Type)
}
if !reflect.DeepEqual(expected, result) {
expectedYaml, _ := yaml.Marshal(expected)
resultYaml, _ := yaml.Marshal(result)
t.Errorf("Structure mismatch: \nexpected:\n %s\ngot:\n %s", string(expectedYaml), string(resultYaml))
}
}
+214
View File
@@ -0,0 +1,214 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestVless_Basic_SimpleLink(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:8080#VLESS%20Proxy"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS Proxy",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 8080,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithTLS(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?security=tls&sni=example.com&alpn=h2,http/1.1#VLESS%20TLS"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS TLS",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
TLS: true,
ALPN: []string{"h2", "http/1.1"},
ServerName: "example.com",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithReality(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?security=reality&sni=example.com&pbk=publickey123&sid=shortid123&fp=chrome#VLESS%20Reality"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS Reality",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
TLS: true,
ServerName: "example.com",
RealityOpts: proxy.RealityOptions{
PublicKey: "publickey123",
ShortID: "shortid123",
},
Fingerprint: "chrome",
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithWebSocket(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=ws&path=/ws&host=example.com#VLESS%20WS"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS WS",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/ws",
Headers: map[string]string{
"Host": "example.com",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithGrpc(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=grpc&serviceName=grpc_service#VLESS%20gRPC"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS gRPC",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
Network: "grpc",
GrpcOpts: proxy.GrpcOptions{
GrpcServiceName: "grpc_service",
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Basic_WithHTTP(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:443?type=http&path=/path1,/path2&host=host1.com,host2.com#VLESS%20HTTP"
expected := proxy.Proxy{
Type: "vless",
Name: "VLESS HTTP",
Vless: proxy.Vless{
Server: "127.0.0.1",
Port: 443,
UUID: "b831b0c4-33b7-4873-9834-28d66d87d4ce",
Network: "http",
HTTPOpts: proxy.HTTPOptions{
Path: []string{"/path1", "/path2"},
Headers: map[string][]string{
"host": {"host1.com", "host2.com"},
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVless_Error_MissingServer(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVless_Error_MissingPort(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVless_Error_InvalidPort(t *testing.T) {
p := &parser.VlessParser{}
input := "vless://b831b0c4-33b7-4873-9834-28d66d87d4ce@127.0.0.1:99999"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVless_Error_InvalidProtocol(t *testing.T) {
p := &parser.VlessParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
+232
View File
@@ -0,0 +1,232 @@
package test
import (
"testing"
"github.com/bestnite/sub2clash/model/proxy"
"github.com/bestnite/sub2clash/parser"
)
func TestVmess_Basic_SimpleLink(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithPath(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBhdGgiOiIvd3MiLCJwb3J0IjoiNDQzIiwicHMiOiJIQUNLIiwidGxzIjoidGxzIiwidHlwZSI6Im5vbmUiLCJ2IjoiMiJ9"
expected := proxy.Proxy{
Type: "vmess",
Name: "HACK",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/ws",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithHost(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaG9zdCI6ImV4YW1wbGUuY29tIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "example.com",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithSNI(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJzbmkiOiJleGFtcGxlLmNvbSIsInRscyI6InRscyIsInR5cGUiOiJub25lIiwidiI6IjIifQ=="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
ServerName: "example.com",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_WithAlterID(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIxIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJ3cyIsInBvcnQiOiI0NDMiLCJwcyI6IkhBSEEiLCJ0bHMiOiJ0bHMiLCJ0eXBlIjoibm9uZSIsInYiOiIyIn0="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 1,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "ws",
WSOpts: proxy.WSOptions{
Path: "/",
Headers: map[string]string{
"Host": "127.0.0.1",
},
},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Basic_GRPC(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJhZGQiOiIxMjcuMC4wLjEiLCJhaWQiOiIwIiwiaWQiOiIxMjM0NTY3OC05MDEyLTM0NTYtNzg5MC0xMjM0NTY3ODkwMTIiLCJuZXQiOiJncnBjIiwicG9ydCI6IjQ0MyIsInBzIjoiSEFIQSIsInRscyI6InRscyIsInR5cGUiOiJub25lIiwidiI6IjIifQ=="
expected := proxy.Proxy{
Type: "vmess",
Name: "HAHA",
Vmess: proxy.Vmess{
UUID: "12345678-9012-3456-7890-123456789012",
AlterID: 0,
Cipher: "auto",
Server: "127.0.0.1",
Port: 443,
TLS: true,
Network: "grpc",
GrpcOpts: proxy.GrpcOptions{},
},
}
result, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
validateResult(t, expected, result)
}
func TestVmess_Error_InvalidBase64(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://invalid_base64"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVmess_Error_InvalidJSON(t *testing.T) {
p := &parser.VmessParser{}
input := "vmess://eyJpbnZhbGlkIjoianNvbn0="
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}
func TestVmess_Error_InvalidProtocol(t *testing.T) {
p := &parser.VmessParser{}
input := "ss://example.com:8080"
_, err := p.Parse(parser.ParseConfig{UseUDP: false}, input)
if err == nil {
t.Errorf("Expected error but got none")
}
}

Some files were not shown because too many files have changed in this diff Show More