package render import ( "encoding/json" "fmt" "net/url" "strings" "github.com/peterqiu0516/sub-store/internal/model" "github.com/peterqiu0516/sub-store/internal/util" ) // RenderProxyUris renders proxies as URI lines (one per line). func RenderProxyUris(proxies []model.ProxyNode) string { var lines []string for _, p := range proxies { if uri := ToProxyUri(p); uri != "" { lines = append(lines, uri) } } return strings.Join(lines, "\n") } // ToProxyUri reconstructs a URI for each supported protocol. // Returns "" for unsupported proxy types. func ToProxyUri(proxy model.ProxyNode) string { ptype := getString(proxy, "type") server := getString(proxy, "server") port := getInt(proxy, "port") name := getString(proxy, "name") switch ptype { case "vless": params := url.Values{} realityOpts, _ := proxy["reality-opts"].(map[string]any) params.Set("encryption", strOr(proxy, "encryption", "none")) if realityOpts != nil { params.Set("security", "reality") } else if getBool(proxy, "tls") { params.Set("security", "tls") } else { params.Set("security", "none") } if sni := getString(proxy, "servername"); sni != "" { params.Set("sni", sni) } if fp := getString(proxy, "client-fingerprint"); fp != "" { params.Set("fp", fp) } if realityOpts != nil { if pk := stringSetting(realityOpts["public-key"]); pk != "" { params.Set("pbk", pk) } if sid := stringSetting(realityOpts["short-id"]); sid != "" { params.Set("sid", sid) } if pk := stringSetting(realityOpts["public-key"]); pk != "" { params.Set("spx", stringSetting(realityOpts["spider-x"])) if params.Get("spx") == "" { params.Set("spx", "/") } } } params.Set("type", strOr(proxy, "network", "tcp")) if flow := getString(proxy, "flow"); flow != "" { params.Set("flow", flow) } return fmt.Sprintf("vless://%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "uuid")), server, port, params.Encode(), url.PathEscape(name)) case "hysteria2": params := url.Values{} if sni := getString(proxy, "sni"); sni != "" { params.Set("sni", sni) } if getBool(proxy, "skip-cert-verify") { params.Set("insecure", "1") } if obfs := getString(proxy, "obfs"); obfs != "" { params.Set("obfs", obfs) } if obfsPwd := getString(proxy, "obfs-password"); obfsPwd != "" { params.Set("obfs-password", obfsPwd) } return fmt.Sprintf("hysteria2://%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "password")), server, port, params.Encode(), url.PathEscape(name)) case "hysteria": params := url.Values{} if sni := getString(proxy, "sni"); sni != "" { params.Set("sni", sni) } if getBool(proxy, "skip-cert-verify") { params.Set("insecure", "1") } if protocol := getString(proxy, "protocol"); protocol != "" { params.Set("protocol", protocol) } if up := stringSetting(proxy["up"]); up != "" { params.Set("up", up) } if down := stringSetting(proxy["down"]); down != "" { params.Set("down", down) } if obfs := getString(proxy, "obfs"); obfs != "" { params.Set("obfs", obfs) } if obfsPwd := getString(proxy, "obfs-password"); obfsPwd != "" { params.Set("obfs-password", obfsPwd) } return fmt.Sprintf("hysteria://%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "auth_str")), server, port, params.Encode(), url.PathEscape(name)) case "anytls": params := url.Values{} if sni := sniStr(proxy, "sni", "servername"); sni != "" { params.Set("sni", sni) } if getBool(proxy, "skip-cert-verify") { params.Set("insecure", "1") } if fp := getString(proxy, "client-fingerprint"); fp != "" { params.Set("fp", fp) } return fmt.Sprintf("anytls://%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "password")), server, port, params.Encode(), url.PathEscape(name)) case "tuic": params := url.Values{} if sni := getString(proxy, "sni"); sni != "" { params.Set("sni", sni) } if getBool(proxy, "skip-cert-verify") { params.Set("allow_insecure", "1") } if getBool(proxy, "disable-sni") { params.Set("disable_sni", "1") } if getBool(proxy, "reduce-rtt") { params.Set("reduce_rtt", "1") } if mode := getString(proxy, "udp-relay-mode"); mode != "" { params.Set("udp_relay_mode", mode) } if cc := getString(proxy, "congestion-controller"); cc != "" { params.Set("congestion_control", cc) } return fmt.Sprintf("tuic://%s:%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "uuid")), url.PathEscape(getString(proxy, "password")), server, port, params.Encode(), url.PathEscape(name)) case "trojan": params := url.Values{} if sni := getString(proxy, "sni"); sni != "" { params.Set("sni", sni) } if getBool(proxy, "skip-cert-verify") { params.Set("allowInsecure", "1") } return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "password")), server, port, params.Encode(), url.PathEscape(name)) case "socks5": params := url.Values{} if getBool(proxy, "tls") { params.Set("tls", "1") } scheme := "socks5" if getBool(proxy, "tls") { scheme = "socks5+tls" } auth := "" if username := getString(proxy, "username"); username != "" { auth = url.PathEscape(username) + ":" + url.PathEscape(getString(proxy, "password")) + "@" } query := "" if len(params) > 0 { query = "?" + params.Encode() } return fmt.Sprintf("%s://%s%s:%d%s#%s", scheme, auth, server, port, query, url.QueryEscape(name)) case "http": auth := "" if username := getString(proxy, "username"); username != "" { auth = url.PathEscape(username) + ":" + url.PathEscape(getString(proxy, "password")) + "@" } scheme := "http" if getBool(proxy, "tls") { scheme = "https" } return fmt.Sprintf("%s://%s%s:%d#%s", scheme, auth, server, port, url.PathEscape(name)) case "ss": userInfo := util.Base64Utf8(fmt.Sprintf("%s:%s@%s:%d", getString(proxy, "cipher"), getString(proxy, "password"), server, port)) return fmt.Sprintf("ss://%s#%s", userInfo, url.PathEscape(name)) case "ssr": main := strings.Join([]string{ server, itoa(port), strOr(proxy, "protocol", "origin"), strOr(proxy, "cipher", "aes-256-cfb"), strOr(proxy, "obfs", "plain"), util.EncodeBase64RawURL(getString(proxy, "password")), }, ":") params := url.Values{} params.Set("remarks", util.EncodeBase64RawURL(name)) if pp := stringSetting(proxy["protocol-param"]); pp != "" { params.Set("protoparam", util.EncodeBase64RawURL(pp)) } if op := stringSetting(proxy["obfs-param"]); op != "" { params.Set("obfsparam", util.EncodeBase64RawURL(op)) } return "ssr://" + util.EncodeBase64RawURL(main+"/?"+params.Encode()) case "wireguard": params := url.Values{} if ip := stringSetting(proxy["ip"]); ip != "" { params.Set("ip", ip) } if ipv6 := stringSetting(proxy["ipv6"]); ipv6 != "" { params.Set("ipv6", ipv6) } if pk := stringSetting(proxy["public-key"]); pk != "" { params.Set("public-key", pk) } if psk := stringSetting(proxy["pre-shared-key"]); psk != "" { params.Set("pre-shared-key", psk) } if reserved := stringSetting(proxy["reserved"]); reserved != "" { params.Set("reserved", reserved) } return fmt.Sprintf("wireguard://%s@%s:%d?%s#%s", url.PathEscape(getString(proxy, "private-key")), server, port, params.Encode(), url.PathEscape(name)) case "vmess": wsOpts, _ := proxy["ws-opts"].(map[string]any) host := "" path := "" if wsOpts != nil { host = stringSetting(WsHeaderHostKey(wsOpts)) if p, ok := wsOpts["path"].(string); ok { path = p } } vmessJSON, _ := json.Marshal(map[string]any{ "v": "2", "ps": name, "add": server, "port": func() string { if port == 0 { return "" } return itoa(port) }(), "id": getString(proxy, "uuid"), "aid": aidStr(proxy["alterId"]), "scy": strOr(proxy, "cipher", "auto"), "tls": boolToStr(getBool(proxy, "tls"), "tls", ""), "sni": getString(proxy, "servername"), "net": strOr(proxy, "network", "tcp"), "type": "none", "host": host, "path": path, }) return "vmess://" + util.Base64Utf8(string(vmessJSON)) } return "" } func sniStr(proxy model.ProxyNode, keys ...string) string { for _, k := range keys { if s := getString(proxy, k); s != "" { return s } } return "" } func aidStr(v any) string { switch n := v.(type) { case nil: return "0" case int: return itoa(n) case int64: return itoa(int(n)) case float64: return itoa(int(n)) case string: if n == "" { return "0" } return n } return "0" } func boolToStr(b bool, trueVal, falseVal string) string { if b { return trueVal } return falseVal } // WsHeaderHostKey extracts Host from ws-opts headers map (case-insensitive). func WsHeaderHostKey(wsOpts map[string]any) string { headers, ok := wsOpts["headers"].(map[string]any) if !ok { return "" } if v, ok := headers["Host"]; ok { return stringSetting(v) } if v, ok := headers["host"]; ok { return stringSetting(v) } return "" }