chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -0,0 +1,651 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user