package subscription import ( "encoding/base64" "net/url" "strconv" "strings" ) // parseSS parses a ss:// URI into a Clash proxy config map. // Direct port of SubscriptionManager._parse_ss. func parseSS(uri string) map[string]interface{} { if !strings.HasPrefix(uri, "ss://") { return nil } name := "ss" if idx := strings.Index(uri, "#"); idx != -1 { fragment := uri[idx+1:] uri = uri[:idx] if unescaped, err := url.QueryUnescape(fragment); err == nil { name = unescaped } else { name = fragment } } content := uri[len("ss://"):] var server, method, password string var port int if idx := strings.Index(content, "@"); idx != -1 { userinfo := content[:idx] hostport := content[idx+1:] // Try to decode userinfo first, it might be base64(method:password). decoded, err := base64URLDecodeWithPadding(userinfo) if err == nil { if mIdx := strings.Index(decoded, ":"); mIdx != -1 { method = decoded[:mIdx] password = decoded[mIdx+1:] } } if method == "" { if mIdx := strings.Index(userinfo, ":"); mIdx != -1 { method = userinfo[:mIdx] password = userinfo[mIdx+1:] } } if method == "" || password == "" { return nil } hIdx := strings.Index(hostport, ":") if hIdx == -1 { return nil } server = hostport[:hIdx] p, err := strconv.Atoi(hostport[hIdx+1:]) if err != nil { return nil } port = p } else { // Entire body is likely base64 encoded. decoded, err := base64URLDecodeWithPadding(content) if err != nil { return nil } if !strings.Contains(decoded, "@") { return nil } atIdx := strings.Index(decoded, "@") methodPass := decoded[:atIdx] hostPort := decoded[atIdx+1:] mIdx := strings.Index(methodPass, ":") if mIdx == -1 { return nil } method = methodPass[:mIdx] password = methodPass[mIdx+1:] hIdx := strings.Index(hostPort, ":") if hIdx == -1 { return nil } server = hostPort[:hIdx] p, err := strconv.Atoi(hostPort[hIdx+1:]) if err != nil { return nil } port = p } return map[string]interface{}{ "name": name, "type": "ss", "server": server, "port": port, "cipher": method, "password": password, } } // stripBase64Whitespace removes whitespace, matching Python's // base64.b64decode/urlsafe_b64decode default validate=False leniency, which // silently ignores embedded newlines/whitespace (common in HTTP response // bodies) rather than erroring like Go's strict decoder. func stripBase64Whitespace(s string) string { return strings.Join(strings.Fields(s), "") } // base64URLDecodeWithPadding decodes base64 (URL-safe alphabet), adding // padding as needed, matching the Python `base64.urlsafe_b64decode` + manual // padding fixup used throughout subscription_manager.py. func base64URLDecodeWithPadding(s string) (string, error) { s = stripBase64Whitespace(s) if pad := len(s) % 4; pad != 0 { s += strings.Repeat("=", 4-pad) } decoded, err := base64.URLEncoding.DecodeString(s) if err != nil { return "", err } return string(decoded), nil } // base64StdDecodeWithPadding decodes standard-alphabet base64, adding padding // as needed, matching `base64.b64decode` usage in _convert_content. func base64StdDecodeWithPadding(s string) (string, error) { s = stripBase64Whitespace(s) if pad := len(s) % 4; pad != 0 { s += strings.Repeat("=", 4-pad) } decoded, err := base64.StdEncoding.DecodeString(s) if err != nil { return "", err } return string(decoded), nil } // parseTrojan parses a trojan:// URI into a Clash proxy config map. // Direct port of SubscriptionManager._parse_trojan. func parseTrojan(uri string) map[string]interface{} { if !strings.HasPrefix(uri, "trojan://") { return nil } parsed, err := url.Parse(uri) if err != nil { return nil } query := parsed.Query() name := "trojan" if parsed.Fragment != "" { if unescaped, err := url.QueryUnescape(parsed.Fragment); err == nil { name = unescaped } else { name = parsed.Fragment } } port, _ := strconv.Atoi(parsed.Port()) config := map[string]interface{}{ "name": name, "type": "trojan", "server": parsed.Hostname(), "port": port, "password": passwordFromUserinfo(parsed), "udp": true, } if v := query.Get("sni"); v != "" { config["sni"] = v } else if v := query.Get("peer"); v != "" { config["sni"] = v } if v := query.Get("allowInsecure"); v != "" { config["skip-cert-verify"] = v == "1" } return config } // passwordFromUserinfo returns the username component of a parsed URL, which // Python's urllib.parse exposes as `.username` (the trojan/vless password or // UUID is encoded there). func passwordFromUserinfo(u *url.URL) string { if u.User == nil { return "" } return u.User.Username() } // parseVless parses a vless:// URI into a Clash proxy config map. // Direct port of SubscriptionManager._parse_vless. func parseVless(uri string) map[string]interface{} { if !strings.HasPrefix(uri, "vless://") { return nil } parsed, err := url.Parse(uri) if err != nil { return nil } query := parsed.Query() name := "vless" if parsed.Fragment != "" { if unescaped, err := url.QueryUnescape(parsed.Fragment); err == nil { name = unescaped } else { name = parsed.Fragment } } port, _ := strconv.Atoi(parsed.Port()) config := map[string]interface{}{ "name": name, "type": "vless", "server": parsed.Hostname(), "port": port, "uuid": passwordFromUserinfo(parsed), "udp": true, "tls": false, "network": "tcp", } if sec := query.Get("security"); sec != "" { if sec == "tls" { config["tls"] = true } else if sec == "reality" { config["tls"] = true realityOpts := map[string]interface{}{} if v := query.Get("pbk"); v != "" { realityOpts["public-key"] = v } if v := query.Get("sid"); v != "" { realityOpts["short-id"] = v } if v := query.Get("sni"); v != "" { config["servername"] = v } config["reality-opts"] = realityOpts } } if v := query.Get("flow"); v != "" { config["flow"] = v } if v := query.Get("type"); v != "" { config["network"] = v } if v := query.Get("sni"); v != "" { config["servername"] = v } if v := query.Get("fp"); v != "" { config["client-fingerprint"] = v } return config }