347 lines
8.9 KiB
Go
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 ""
|
|
}
|