Files
sub-store/internal/render/uri.go
T

347 lines
8.9 KiB
Go

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.QueryEscape(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.QueryEscape(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.QueryEscape(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.QueryEscape(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.QueryEscape(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.QueryEscape(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.QueryEscape(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.QueryEscape(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.QueryEscape(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 ""
}