652 lines
22 KiB
Go
652 lines
22 KiB
Go
package proxy
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/peterqiu0516/sub-store/internal/model"
|
|
)
|
|
|
|
var (
|
|
// qxKindRegex matches QX-style proxy lines: "shadowsocks = ...", "vmess = ...", etc.
|
|
qxKindRegex = regexp.MustCompile(`(?i)^\s*(shadowsocks|vmess|vless|trojan|http|socks5|anytls)\s*=`)
|
|
// namedKindRegex matches named Surge/Loon-style lines: "name = kind, ..."
|
|
namedKindRegex = regexp.MustCompile(`^\s*[^=\n]{1,120}\s*=`)
|
|
)
|
|
|
|
// ParseClientProxyLine dispatches a client config line to QX or named parser.
|
|
// Returns nil if the line doesn't match either format.
|
|
func ParseClientProxyLine(line string, index int) model.ProxyNode {
|
|
defer func() { _ = recover() }()
|
|
if qxKindRegex.MatchString(line) {
|
|
return ParseQxProxyLine(line, index)
|
|
}
|
|
if namedKindRegex.MatchString(line) {
|
|
return ParseNamedClientProxyLine(line, index)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParseQxProxyLine parses a Quantumult X style proxy config line.
|
|
// Format: kind = server:port, tag=name, ...
|
|
func ParseQxProxyLine(line string, index int) model.ProxyNode {
|
|
equalIndex := strings.Index(line, "=")
|
|
if equalIndex <= 0 {
|
|
return nil
|
|
}
|
|
kind := strings.ToLower(strings.TrimSpace(line[:equalIndex]))
|
|
parts := SplitClientCsv(line[equalIndex+1:])
|
|
if len(parts) == 0 {
|
|
return nil
|
|
}
|
|
server, rawPort := SplitHostPort(parts[0])
|
|
options := ParseClientOptions(parts[1:])
|
|
name := orDefault(ClientOption(options, "tag"), fmt.Sprintf("%s-%d", kind, index+1))
|
|
|
|
defaultPort := 443
|
|
if kind == "http" || kind == "socks5" {
|
|
defaultPort = 80
|
|
}
|
|
port := toFloat(rawPort)
|
|
if port == 0 {
|
|
if p := toFloat(ClientOption(options, "port")); p != 0 {
|
|
port = p
|
|
} else {
|
|
port = float64(defaultPort)
|
|
}
|
|
}
|
|
|
|
tls := qxTlsEnabled(options)
|
|
common := ClientCommonOptions(options)
|
|
|
|
switch kind {
|
|
case "shadowsocks":
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "ss",
|
|
"server": server,
|
|
"port": port,
|
|
"cipher": ClientOption(options, "method"),
|
|
"password": ClientOption(options, "password"),
|
|
"plugin": qxPlugin(options),
|
|
"plugin-opts": qxPluginOptions(options),
|
|
"udp": OptionBoolean(ClientOption(options, "udp-relay")),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "vmess", "vless":
|
|
node := map[string]any{
|
|
"name": name,
|
|
"type": kind,
|
|
"server": server,
|
|
"port": port,
|
|
"uuid": firstNonEmpty(ClientOption(options, "password"), ClientOption(options, "uuid"), ClientOption(options, "username")),
|
|
"network": qxNetwork(options),
|
|
"tls": tls,
|
|
"servername": firstNonEmpty(ClientOption(options, "tls-host"), ClientOption(options, "obfs-host")),
|
|
"ws-opts": qxWsOptions(options),
|
|
"reality-opts": parseRealityOptions(options),
|
|
"flow": ClientOption(options, "flow"),
|
|
"udp": OptionBoolean(ClientOption(options, "udp-relay")),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
}
|
|
if kind == "vmess" {
|
|
node["cipher"] = orDefault(ClientOption(options, "method"), "auto")
|
|
node["alterId"] = NumberOrUndefined(firstNonEmpty(ClientOption(options, "alterId"), ClientOption(options, "alterid")))
|
|
} else {
|
|
node["encryption"] = orDefault(ClientOption(options, "encryption"), "none")
|
|
}
|
|
return StripUndefined(node)
|
|
case "trojan", "anytls":
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": kind,
|
|
"server": server,
|
|
"port": port,
|
|
"password": ClientOption(options, "password"),
|
|
"sni": firstNonEmpty(ClientOption(options, "tls-host"), ClientOption(options, "sni"), ClientOption(options, "obfs-host")),
|
|
"reality-opts": parseRealityOptions(options),
|
|
"udp": OptionBoolean(ClientOption(options, "udp-relay")),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "http", "socks5":
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": kind,
|
|
"server": server,
|
|
"port": port,
|
|
"username": ClientOption(options, "username"),
|
|
"password": ClientOption(options, "password"),
|
|
"tls": tls,
|
|
"udp": OptionBoolean(ClientOption(options, "udp-relay")),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ParseNamedClientProxyLine parses a Surge/Loon style named proxy config line.
|
|
// Format: name = kind, server, port, [positional...], [key=value...]
|
|
func ParseNamedClientProxyLine(line string, index int) model.ProxyNode {
|
|
equalIndex := strings.Index(line, "=")
|
|
if equalIndex < 0 {
|
|
return nil
|
|
}
|
|
name := strings.TrimSpace(line[:equalIndex])
|
|
if name == "" {
|
|
name = fmt.Sprintf("proxy-%d", index+1)
|
|
}
|
|
parts := SplitClientCsv(line[equalIndex+1:])
|
|
if len(parts) < 3 {
|
|
return nil
|
|
}
|
|
rawKind := strings.ToLower(strings.TrimSpace(parts[0]))
|
|
kind := NormalizeClientProxyKind(parts[0])
|
|
server := parts[1]
|
|
port := toFloat(parts[2])
|
|
positional := parts[3:]
|
|
var positionalValues []string
|
|
for _, p := range positional {
|
|
if !strings.Contains(p, "=") {
|
|
positionalValues = append(positionalValues, p)
|
|
}
|
|
}
|
|
options := ParseClientOptions(positional)
|
|
common := ClientCommonOptions(options)
|
|
|
|
if kind == "" || server == "" || port == 0 {
|
|
return nil
|
|
}
|
|
|
|
switch kind {
|
|
case "ss":
|
|
var pluginOpts any
|
|
if obfs := firstNonEmpty(ClientOption(options, "obfs"), ClientOption(options, "obfs-name")); obfs != "" {
|
|
pluginOpts = StripUndefined(map[string]any{
|
|
"mode": obfs,
|
|
"host": ClientOption(options, "obfs-host"),
|
|
"path": ClientOption(options, "obfs-uri"),
|
|
})
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "ss",
|
|
"server": server,
|
|
"port": port,
|
|
"cipher": firstNonEmpty(ClientOption(options, "encrypt-method"), ClientOption(options, "method"), getAt(positionalValues, 0)),
|
|
"password": firstNonEmpty(ClientOption(options, "password"), getAt(positionalValues, 1)),
|
|
"plugin": ternary(firstNonEmpty(ClientOption(options, "obfs"), ClientOption(options, "obfs-name")) != "", "obfs", ""),
|
|
"plugin-opts": pluginOpts,
|
|
"udp": OptionBoolean(firstNonEmpty(ClientOption(options, "udp"), ClientOption(options, "udp-relay"))),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "ssr":
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "ssr",
|
|
"server": server,
|
|
"port": port,
|
|
"cipher": firstNonEmpty(getAt(positionalValues, 0), ClientOption(options, "encrypt-method"), ClientOption(options, "method")),
|
|
"password": firstNonEmpty(getAt(positionalValues, 1), ClientOption(options, "password")),
|
|
"protocol": orDefault(ClientOption(options, "protocol"), "origin"),
|
|
"obfs": orDefault(ClientOption(options, "obfs"), "plain"),
|
|
"protocol-param": firstNonEmpty(ClientOption(options, "protocol-param"), ClientOption(options, "protoparam")),
|
|
"obfs-param": firstNonEmpty(ClientOption(options, "obfs-param"), ClientOption(options, "obfsparam")),
|
|
"udp": OptionBoolean(firstNonEmpty(ClientOption(options, "udp"), ClientOption(options, "udp-relay"))),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "vmess", "vless":
|
|
tls := false
|
|
if b := OptionBoolean(firstNonEmpty(ClientOption(options, "tls"), ClientOption(options, "over-tls"))); b != nil {
|
|
tls = *b
|
|
}
|
|
node := map[string]any{
|
|
"name": name,
|
|
"type": kind,
|
|
"server": server,
|
|
"port": port,
|
|
"uuid": firstNonEmpty(ClientOption(options, "username"), ClientOption(options, "password"), getAt(positionalValues, 1), getAt(positionalValues, 0)),
|
|
"network": namedClientNetwork(options),
|
|
"tls": tls,
|
|
"servername": firstNonEmpty(ClientOption(options, "sni"), ClientOption(options, "tls-name"), ClientOption(options, "tls-host")),
|
|
"ws-opts": namedClientWsOptions(options),
|
|
"reality-opts": parseRealityOptions(options),
|
|
"flow": ClientOption(options, "flow"),
|
|
"udp": OptionBoolean(firstNonEmpty(ClientOption(options, "udp"), ClientOption(options, "udp-relay"))),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
}
|
|
if kind == "vmess" {
|
|
node["cipher"] = firstNonEmpty(getAt(positionalValues, 0), ClientOption(options, "encrypt-method"), ClientOption(options, "method"), "auto")
|
|
node["alterId"] = NumberOrUndefined(firstNonEmpty(ClientOption(options, "alterId"), ClientOption(options, "alterid")))
|
|
} else {
|
|
node["encryption"] = orDefault(ClientOption(options, "encryption"), "none")
|
|
}
|
|
return StripUndefined(node)
|
|
case "trojan", "anytls":
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": kind,
|
|
"server": server,
|
|
"port": port,
|
|
"password": firstNonEmpty(ClientOption(options, "password"), getAt(positionalValues, 0)),
|
|
"sni": firstNonEmpty(ClientOption(options, "sni"), ClientOption(options, "tls-name"), ClientOption(options, "tls-host")),
|
|
"reality-opts": parseRealityOptions(options),
|
|
"udp": OptionBoolean(firstNonEmpty(ClientOption(options, "udp"), ClientOption(options, "udp-relay"))),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "http", "socks5":
|
|
tls := rawKind == "https" || rawKind == "socks5-tls"
|
|
if b := OptionBoolean(firstNonEmpty(ClientOption(options, "tls"), ClientOption(options, "over-tls"))); b != nil {
|
|
tls = *b
|
|
}
|
|
proxyType := kind
|
|
if kind == "socks5" {
|
|
proxyType = "socks5"
|
|
} else {
|
|
proxyType = "http"
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": proxyType,
|
|
"server": server,
|
|
"port": port,
|
|
"username": ClientOption(options, "username"),
|
|
"password": ClientOption(options, "password"),
|
|
"tls": tls,
|
|
"udp": OptionBoolean(firstNonEmpty(ClientOption(options, "udp"), ClientOption(options, "udp-relay"))),
|
|
"tfo": OptionBoolean(ClientOption(options, "fast-open")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "hysteria2":
|
|
scv := common[skipCertVerify]
|
|
if b := OptionBoolean(ClientOption(options, "skip-cert-verify")); b != nil {
|
|
scv = *b
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "hysteria2",
|
|
"server": server,
|
|
"port": port,
|
|
"password": firstNonEmpty(ClientOption(options, "password"), getAt(positionalValues, 0)),
|
|
"sni": firstNonEmpty(ClientOption(options, "sni"), ClientOption(options, "tls-name")),
|
|
"obfs": ClientOption(options, "obfs"),
|
|
"obfs-password": firstNonEmpty(ClientOption(options, "obfs-password"), ClientOption(options, "gecko-password")),
|
|
skipCertVerify: scv,
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "tuic":
|
|
scv := common[skipCertVerify]
|
|
if b := OptionBoolean(ClientOption(options, "skip-cert-verify")); b != nil {
|
|
scv = *b
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "tuic",
|
|
"server": server,
|
|
"port": port,
|
|
"uuid": firstNonEmpty(ClientOption(options, "uuid"), getAt(positionalValues, 0)),
|
|
"password": firstNonEmpty(ClientOption(options, "password"), getAt(positionalValues, 1)),
|
|
"sni": ClientOption(options, "sni"),
|
|
"alpn": CommaList(ClientOption(options, "alpn")),
|
|
skipCertVerify: scv,
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "snell":
|
|
version := NumberOrUndefined(ClientOption(options, "version"))
|
|
if version == nil {
|
|
version = float64(3)
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "snell",
|
|
"server": server,
|
|
"port": port,
|
|
"psk": firstNonEmpty(ClientOption(options, "psk"), ClientOption(options, "password"), getAt(positionalValues, 0)),
|
|
"version": version,
|
|
"obfs": ClientOption(options, "obfs"),
|
|
"obfs-host": ClientOption(options, "obfs-host"),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "ssh":
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "ssh",
|
|
"server": server,
|
|
"port": port,
|
|
"username": firstNonEmpty(ClientOption(options, "username"), getAt(positionalValues, 0)),
|
|
"password": firstNonEmpty(ClientOption(options, "password"), getAt(positionalValues, 1)),
|
|
"private-key": ClientOption(options, "private-key"),
|
|
"host-key": ClientOption(options, "host-key"),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
case "h2-connect":
|
|
tls := true
|
|
if b := OptionBoolean(ClientOption(options, "tls")); b != nil {
|
|
tls = *b
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"name": name,
|
|
"type": "h2-connect",
|
|
"server": server,
|
|
"port": port,
|
|
"username": ClientOption(options, "username"),
|
|
"password": ClientOption(options, "password"),
|
|
"tls": tls,
|
|
"sni": firstNonEmpty(ClientOption(options, "sni"), ClientOption(options, "tls-name")),
|
|
skipCertVerify: common[skipCertVerify],
|
|
clientFingerprint: common[clientFingerprint],
|
|
})
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NormalizeClientProxyKind maps client proxy kind aliases to canonical names.
|
|
// Per review-resolution #23.
|
|
func NormalizeClientProxyKind(input string) string {
|
|
value := strings.ToLower(strings.TrimSpace(input))
|
|
switch value {
|
|
case "shadowsocks":
|
|
return "ss"
|
|
case "socks5-tls":
|
|
return "socks5"
|
|
case "https":
|
|
return "http"
|
|
case "hysteria2", "hysteria 2":
|
|
return "hysteria2"
|
|
case "tuic-v5":
|
|
return "tuic"
|
|
case "ss", "ssr", "vmess", "vless", "trojan", "http", "socks5", "tuic", "anytls", "snell", "ssh", "h2-connect":
|
|
return value
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// SplitClientCsv splits a CSV string with quote handling.
|
|
// Empty parts are filtered out (matching TS implementation).
|
|
func SplitClientCsv(input string) []string {
|
|
var parts []string
|
|
var current strings.Builder
|
|
quote := ""
|
|
for _, char := range input {
|
|
if quote != "" {
|
|
if string(char) == quote {
|
|
quote = ""
|
|
} else {
|
|
current.WriteRune(char)
|
|
}
|
|
} else if char == '"' || char == '\'' {
|
|
quote = string(char)
|
|
} else if char == ',' {
|
|
parts = append(parts, strings.TrimSpace(current.String()))
|
|
current.Reset()
|
|
} else {
|
|
current.WriteRune(char)
|
|
}
|
|
}
|
|
parts = append(parts, strings.TrimSpace(current.String()))
|
|
// Filter empty parts
|
|
var result []string
|
|
for _, p := range parts {
|
|
if p != "" {
|
|
result = append(result, p)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ParseClientOptions parses key=value parts into a map.
|
|
// Keys are lowercased. Values are unquoted.
|
|
func ParseClientOptions(parts []string) map[string]string {
|
|
options := make(map[string]string)
|
|
for _, part := range parts {
|
|
equalIndex := strings.Index(part, "=")
|
|
if equalIndex <= 0 {
|
|
continue
|
|
}
|
|
key := strings.ToLower(strings.TrimSpace(part[:equalIndex]))
|
|
value := unquoteClientValue(strings.TrimSpace(part[equalIndex+1:]))
|
|
options[key] = value
|
|
}
|
|
return options
|
|
}
|
|
|
|
// ClientOption retrieves a value from the options map (case-insensitive key already lowered).
|
|
func ClientOption(options map[string]string, key string) string {
|
|
return options[strings.ToLower(key)]
|
|
}
|
|
|
|
// OptionBoolean parses a string value into a *bool.
|
|
// Returns nil for empty/unrecognized values.
|
|
// Per review-resolution: "1","true","yes","on","enabled" → true;
|
|
// "0","false","no","off","disabled" → false; otherwise nil.
|
|
func OptionBoolean(value string) *bool {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
switch normalized {
|
|
case "1", "true", "yes", "on", "enabled":
|
|
b := true
|
|
return &b
|
|
case "0", "false", "no", "off", "disabled":
|
|
b := false
|
|
return &b
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ClientCommonOptions extracts common options (skip-cert-verify, client-fingerprint).
|
|
func ClientCommonOptions(options map[string]string) map[string]any {
|
|
result := map[string]any{}
|
|
scv := OptionBoolean(ClientOption(options, "skip-cert-verify"))
|
|
if scv == nil {
|
|
scv = OptionBooleanInverted(ClientOption(options, "tls-verification"))
|
|
}
|
|
if scv != nil {
|
|
result[skipCertVerify] = *scv
|
|
}
|
|
if fp := firstNonEmpty(ClientOption(options, "client-fingerprint"), ClientOption(options, "fingerprint")); fp != "" {
|
|
result[clientFingerprint] = fp
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Constants for common option keys to avoid typos.
|
|
const (
|
|
skipCertVerify = "skip-cert-verify"
|
|
clientFingerprint = "client-fingerprint"
|
|
)
|
|
|
|
// OptionBooleanInverted parses a value where the semantics are inverted
|
|
// (e.g. tls-verification where true means don't skip).
|
|
func OptionBooleanInverted(value string) *bool {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
switch normalized {
|
|
case "1", "true", "yes", "on", "enabled":
|
|
b := false
|
|
return &b
|
|
case "0", "false", "no", "off", "disabled":
|
|
b := true
|
|
return &b
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// unquoteClientValue removes surrounding quotes from a value.
|
|
func unquoteClientValue(input string) string {
|
|
text := strings.TrimSpace(input)
|
|
if len(text) < 2 {
|
|
return text
|
|
}
|
|
first := text[0]
|
|
last := text[len(text)-1]
|
|
if (first == '"' || first == '\'') && last == first {
|
|
return text[1 : len(text)-1]
|
|
}
|
|
return text
|
|
}
|
|
|
|
// qxTlsEnabled determines if TLS is enabled for a QX proxy.
|
|
func qxTlsEnabled(options map[string]string) bool {
|
|
obfs := strings.ToLower(ClientOption(options, "obfs"))
|
|
if obfs == "tls" || obfs == "wss" || obfs == "over-tls" {
|
|
return true
|
|
}
|
|
if b := OptionBoolean(ClientOption(options, "over-tls")); b != nil && *b {
|
|
return true
|
|
}
|
|
if b := OptionBoolean(ClientOption(options, "tls")); b != nil && *b {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// qxNetwork determines the network type for a QX proxy.
|
|
func qxNetwork(options map[string]string) string {
|
|
obfs := strings.ToLower(ClientOption(options, "obfs"))
|
|
if obfs == "ws" || obfs == "wss" {
|
|
return "ws"
|
|
}
|
|
return "tcp"
|
|
}
|
|
|
|
// qxWsOptions builds ws-opts for a QX proxy.
|
|
func qxWsOptions(options map[string]string) any {
|
|
if qxNetwork(options) != "ws" {
|
|
return nil
|
|
}
|
|
wsOpts := map[string]any{
|
|
"path": orDefault(ClientOption(options, "obfs-uri"), "/"),
|
|
}
|
|
host := ClientOption(options, "obfs-host")
|
|
if host != "" {
|
|
wsOpts["headers"] = map[string]any{"Host": host}
|
|
}
|
|
return StripUndefined(wsOpts)
|
|
}
|
|
|
|
// qxPlugin determines the plugin name for a QX proxy.
|
|
func qxPlugin(options map[string]string) any {
|
|
obfs := strings.ToLower(ClientOption(options, "obfs"))
|
|
if obfs == "http" || obfs == "shadowsocks-http" {
|
|
return "obfs"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// qxPluginOptions builds plugin-opts for a QX proxy.
|
|
func qxPluginOptions(options map[string]string) any {
|
|
if qxPlugin(options) == nil {
|
|
return nil
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"mode": "http",
|
|
"host": ClientOption(options, "obfs-host"),
|
|
"path": ClientOption(options, "obfs-uri"),
|
|
})
|
|
}
|
|
|
|
// namedClientNetwork determines the network type for a named client proxy.
|
|
func namedClientNetwork(options map[string]string) string {
|
|
if b := OptionBoolean(ClientOption(options, "ws")); b != nil && *b {
|
|
return "ws"
|
|
}
|
|
transport := firstNonEmpty(ClientOption(options, "transport"), ClientOption(options, "network"))
|
|
if transport != "" {
|
|
return transport
|
|
}
|
|
return "tcp"
|
|
}
|
|
|
|
// namedClientWsOptions builds ws-opts for a named client proxy.
|
|
func namedClientWsOptions(options map[string]string) any {
|
|
if namedClientNetwork(options) != "ws" {
|
|
return nil
|
|
}
|
|
wsOpts := map[string]any{
|
|
"path": firstNonEmpty(ClientOption(options, "ws-path"), ClientOption(options, "path"), "/"),
|
|
}
|
|
host := ClientOption(options, "ws-headers")
|
|
if host != "" {
|
|
// Remove leading "Host:" prefix (case-insensitive)
|
|
host = regexp.MustCompile(`(?i)^Host:`).ReplaceAllString(host, "")
|
|
host = strings.TrimSpace(host)
|
|
}
|
|
if host == "" {
|
|
host = firstNonEmpty(ClientOption(options, "ws-host"), ClientOption(options, "host"))
|
|
}
|
|
if host != "" {
|
|
wsOpts["headers"] = map[string]any{"Host": host}
|
|
}
|
|
return StripUndefined(wsOpts)
|
|
}
|
|
|
|
// parseRealityOptions extracts reality-opts from client options.
|
|
func parseRealityOptions(options map[string]string) any {
|
|
publicKey := firstNonEmpty(ClientOption(options, "reality-base64-pubkey"), ClientOption(options, "public-key"))
|
|
shortId := firstNonEmpty(ClientOption(options, "reality-hex-shortid"), ClientOption(options, "short-id"))
|
|
if publicKey == "" {
|
|
return nil
|
|
}
|
|
return StripUndefined(map[string]any{
|
|
"public-key": publicKey,
|
|
"short-id": shortId,
|
|
})
|
|
}
|
|
|
|
// toFloat parses a string to float64, returning 0 on failure.
|
|
func toFloat(s string) float64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
n, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
// getAt returns the element at index i, or "" if out of bounds.
|
|
func getAt(slice []string, i int) string {
|
|
if i < 0 || i >= len(slice) {
|
|
return ""
|
|
}
|
|
return slice[i]
|
|
}
|
|
|
|
// ternary returns a if cond is true, else b.
|
|
func ternary(cond bool, a, b string) string {
|
|
if cond {
|
|
return a
|
|
}
|
|
return b
|
|
}
|