mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 07:37:15 +00:00
feat(sub): add legacy Clash subscription endpoint (#6338)
* feat(sub): add legacy Clash subscription endpoint * fix(deps): update js-yaml to patched release Raise the Swagger UI js-yaml override to 4.3.2 and refresh the lockfile to resolve GHSA-2883-xcg3-v3hh without changing Swagger UI. * fix(sub): preserve client detection and normalize legacy cipher Keep the original Clash/Mihomo auto-detection default so existing subscription URLs continue returning YAML. Normalize the panel-supported chacha20-poly1305 alias when generating legacy Clash profiles, and cover both regressions through HTTP endpoint tests. * refactor(sub): drop an unreachable guard and make the alias test assert Review of the legacy Clash subscription endpoint left three LOW findings, all introduced by the change: - The comment above the routing merge ran to three lines, over CLAUDE.md's two-line cap. - validateClashRouteGraph on the legacy path could never fail: the legacy branch skips the routing merge, so it validated the literal config built a few lines above against itself. Dead code that reads as a guard. - TestClashAliasesSkipConfiguredPathConflicts asserted nothing — it could only fail on an escaping gin panic, so a regression that registered the alias handler on the configured path went unnoticed. It now drives each collision through the router and asserts which format answers each path. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -44,6 +44,85 @@ func TestEnsureUniqueProxyNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyClashProxyCompatibility(t *testing.T) {
|
||||
t.Run("keeps legacy vmess fields", func(t *testing.T) {
|
||||
proxy := map[string]any{
|
||||
"name": "vm", "type": "vmess", "server": "vm.example.com", "port": 443,
|
||||
"uuid": "11111111-2222-4333-8444-555555555555", "alterId": 0, "cipher": "auto",
|
||||
"udp": true, "network": "ws", "tls": true, "servername": "sni.example.com",
|
||||
"ws-opts": map[string]any{"path": "/ws"}, "client-fingerprint": "chrome", "alpn": []string{"h2"},
|
||||
}
|
||||
|
||||
got := legacyClashProxy(proxy)
|
||||
if got == nil || got["type"] != "vmess" || got["network"] != "ws" {
|
||||
t.Fatalf("legacy vmess was filtered or changed: %#v", got)
|
||||
}
|
||||
for _, field := range []string{"client-fingerprint", "alpn"} {
|
||||
if _, exists := got[field]; exists {
|
||||
t.Fatalf("Mihomo-only field %q leaked into legacy vmess: %#v", field, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keeps legacy trojan fields", func(t *testing.T) {
|
||||
proxy := map[string]any{
|
||||
"name": "tr", "type": "trojan", "server": "tr.example.com", "port": 443,
|
||||
"password": "secret", "udp": true, "network": "grpc", "tls": true,
|
||||
"sni": "sni.example.com", "servername": "sni.example.com", "alpn": []string{"h2"},
|
||||
"grpc-opts": map[string]any{"grpc-service-name": "svc"},
|
||||
}
|
||||
|
||||
got := legacyClashProxy(proxy)
|
||||
if got == nil || got["type"] != "trojan" || got["sni"] != "sni.example.com" {
|
||||
t.Fatalf("legacy trojan was filtered or changed: %#v", got)
|
||||
}
|
||||
for _, field := range []string{"tls", "servername"} {
|
||||
if _, exists := got[field]; exists {
|
||||
t.Fatalf("field %q is not part of the legacy Trojan schema: %#v", field, got)
|
||||
}
|
||||
}
|
||||
|
||||
withoutTLS := cloneMap(proxy)
|
||||
withoutTLS["tls"] = false
|
||||
if got := legacyClashProxy(withoutTLS); got != nil {
|
||||
t.Fatalf("Trojan without TLS must not reach Clash for Windows: %#v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keeps only legacy shadowsocks ciphers", func(t *testing.T) {
|
||||
legacy := map[string]any{
|
||||
"name": "ss", "type": "ss", "server": "ss.example.com", "port": 443,
|
||||
"password": "secret", "cipher": "aes-256-gcm", "udp": true, "network": "tcp", "tls": false,
|
||||
}
|
||||
got := legacyClashProxy(legacy)
|
||||
if got == nil || got["type"] != "ss" {
|
||||
t.Fatalf("legacy Shadowsocks proxy was filtered: %#v", got)
|
||||
}
|
||||
for _, field := range []string{"network", "tls"} {
|
||||
if _, exists := got[field]; exists {
|
||||
t.Fatalf("field %q is not part of the legacy Shadowsocks schema: %#v", field, got)
|
||||
}
|
||||
}
|
||||
|
||||
ss2022 := cloneMap(legacy)
|
||||
ss2022["cipher"] = "2022-blake3-aes-256-gcm"
|
||||
if got := legacyClashProxy(ss2022); got != nil {
|
||||
t.Fatalf("SS-2022 must not reach Clash for Windows: %#v", got)
|
||||
}
|
||||
})
|
||||
|
||||
for _, proxy := range []map[string]any{
|
||||
{"name": "vl", "type": "vless"},
|
||||
{"name": "hy", "type": "hysteria2"},
|
||||
{"name": "xh", "type": "vmess", "cipher": "auto", "network": "xhttp"},
|
||||
{"name": "reality", "type": "vmess", "cipher": "auto", "network": "tcp", "reality-opts": map[string]any{}},
|
||||
} {
|
||||
if got := legacyClashProxy(proxy); got != nil {
|
||||
t.Fatalf("modern proxy reached Clash for Windows: %#v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildProxy_VLESSRealityFieldsForClash locks the reality field mapping in
|
||||
// applySecurity (clash_service.go ~488): a regression that drops servername,
|
||||
// public-key, short-id, or client-fingerprint would hand mihomo a broken reality
|
||||
|
||||
Reference in New Issue
Block a user