mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master version: 1.0.0 source-ref: master published-at-utc: 2026-03-13T15:19:28Z
686 lines
18 KiB
Go
686 lines
18 KiB
Go
package proxy
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
// ParseProxyNode 解析代理节点
|
||
func ParseProxyNode(node string) (string, map[string]interface{}, error) {
|
||
src := strings.TrimSpace(node)
|
||
if src == "" {
|
||
return "", nil, fmt.Errorf("代理节点为空")
|
||
}
|
||
l := strings.ToLower(src)
|
||
if strings.HasPrefix(l, "http://") || strings.HasPrefix(l, "https://") || strings.HasPrefix(l, "socks5://") {
|
||
return src, nil, nil
|
||
}
|
||
if strings.HasPrefix(l, "clash://") || strings.Contains(l, "type:") || strings.Contains(l, "proxies:") {
|
||
outbound, standard, err := parseClashNode(src)
|
||
if err != nil {
|
||
return "", nil, err
|
||
}
|
||
if standard != "" {
|
||
return standard, nil, nil
|
||
}
|
||
if outbound != nil {
|
||
return "", outbound, nil
|
||
}
|
||
}
|
||
outbound, err := buildXrayOutbound(src)
|
||
if err != nil {
|
||
return "", nil, err
|
||
}
|
||
return "", outbound, nil
|
||
}
|
||
|
||
func parseClashNode(src string) (map[string]interface{}, string, error) {
|
||
data := strings.TrimSpace(src)
|
||
if strings.HasPrefix(strings.ToLower(data), "clash://") {
|
||
raw := strings.TrimPrefix(data, "clash://")
|
||
raw, _ = url.QueryUnescape(raw)
|
||
decoded, err := decodeBase64String(raw)
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
data = string(decoded)
|
||
}
|
||
var payload interface{}
|
||
if err := yaml.Unmarshal([]byte(data), &payload); err != nil {
|
||
return nil, "", err
|
||
}
|
||
nodeMap := pickClashNode(payload)
|
||
if nodeMap == nil {
|
||
return nil, "", fmt.Errorf("clash 节点解析失败")
|
||
}
|
||
nodeType := strings.ToLower(getMapString(nodeMap, "type"))
|
||
switch nodeType {
|
||
case "socks5", "http", "https":
|
||
return nil, buildStandardProxyFromClash(nodeMap, nodeType), nil
|
||
case "vmess":
|
||
return buildOutboundFromClashVmess(nodeMap)
|
||
case "vless":
|
||
return buildOutboundFromClashVless(nodeMap)
|
||
case "trojan":
|
||
return buildOutboundFromClashTrojan(nodeMap)
|
||
case "ss", "shadowsocks":
|
||
return buildOutboundFromClashSS(nodeMap)
|
||
case "ssr":
|
||
return nil, "", fmt.Errorf("不支持 ShadowsocksR 协议,Xray 不支持 SSR,请使用 SS/vmess/vless/trojan")
|
||
case "hysteria2", "hysteria":
|
||
return buildOutboundFromClashHysteria2(nodeMap)
|
||
}
|
||
return nil, "", fmt.Errorf("不支持的节点类型")
|
||
}
|
||
|
||
func pickClashNode(payload interface{}) map[string]interface{} {
|
||
if m := toStringMap(payload); m != nil {
|
||
if proxies, ok := m["proxies"]; ok {
|
||
if arr, ok := proxies.([]interface{}); ok && len(arr) > 0 {
|
||
return toStringMap(arr[0])
|
||
}
|
||
}
|
||
if proxyItem, ok := m["proxy"]; ok {
|
||
if node := toStringMap(proxyItem); node != nil {
|
||
return node
|
||
}
|
||
}
|
||
return m
|
||
}
|
||
if arr, ok := payload.([]interface{}); ok && len(arr) > 0 {
|
||
return toStringMap(arr[0])
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func buildStandardProxyFromClash(node map[string]interface{}, scheme string) string {
|
||
host := getMapString(node, "server")
|
||
port := getMapInt(node, "port")
|
||
username := getMapString(node, "username")
|
||
password := getMapString(node, "password")
|
||
if host == "" || port == 0 {
|
||
return ""
|
||
}
|
||
address := fmt.Sprintf("%s:%d", host, port)
|
||
if username != "" {
|
||
user := url.UserPassword(username, password)
|
||
return fmt.Sprintf("%s://%s@%s", scheme, user.String(), address)
|
||
}
|
||
return fmt.Sprintf("%s://%s", scheme, address)
|
||
}
|
||
|
||
func buildOutboundFromClashVless(node map[string]interface{}) (map[string]interface{}, string, error) {
|
||
host := getMapString(node, "server")
|
||
port := getMapInt(node, "port")
|
||
id := getMapString(node, "uuid")
|
||
flow := getMapString(node, "flow")
|
||
// sni 和 servername 都要读
|
||
sni := getMapString(node, "sni")
|
||
if sni == "" {
|
||
sni = getMapString(node, "servername")
|
||
}
|
||
network := getMapString(node, "network")
|
||
out := map[string]interface{}{
|
||
"protocol": "vless",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"vnext": []interface{}{
|
||
map[string]interface{}{
|
||
"address": host,
|
||
"port": port,
|
||
"users": []interface{}{
|
||
map[string]interface{}{
|
||
"id": id,
|
||
"flow": flow,
|
||
"encryption": "none",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
stream := map[string]interface{}{}
|
||
tlsVal := strings.ToLower(getMapString(node, "tls"))
|
||
_, hasRealityOpts := node["reality-opts"]
|
||
|
||
if hasRealityOpts {
|
||
// Reality 模式:network 必须显式为 tcp,否则 xray 校验失败
|
||
stream["network"] = "tcp"
|
||
realityOpts := map[string]interface{}{
|
||
"spiderX": "",
|
||
}
|
||
if sni != "" {
|
||
realityOpts["serverName"] = sni
|
||
}
|
||
fingerprint := getMapString(node, "client-fingerprint")
|
||
if fingerprint == "" {
|
||
fingerprint = "chrome"
|
||
}
|
||
realityOpts["fingerprint"] = fingerprint
|
||
if rm := toStringMap(node["reality-opts"]); rm != nil {
|
||
if pbk := getMapString(rm, "public-key"); pbk != "" {
|
||
realityOpts["publicKey"] = pbk
|
||
}
|
||
if sid := getMapString(rm, "short-id"); sid != "" {
|
||
realityOpts["shortId"] = sid
|
||
}
|
||
}
|
||
stream["security"] = "reality"
|
||
stream["realitySettings"] = realityOpts
|
||
} else if getMapBool(node, "tls") || tlsVal == "true" || tlsVal == "tls" {
|
||
// 普通 TLS 模式
|
||
tlsSettings := map[string]interface{}{}
|
||
if sni != "" {
|
||
tlsSettings["serverName"] = sni
|
||
}
|
||
tlsSettings["allowInsecure"] = getMapBool(node, "skip-cert-verify")
|
||
stream["security"] = "tls"
|
||
stream["tlsSettings"] = tlsSettings
|
||
}
|
||
if network == "ws" {
|
||
stream["network"] = "ws"
|
||
ws := map[string]interface{}{}
|
||
if wsOpts, ok := node["ws-opts"]; ok {
|
||
if wsMap := toStringMap(wsOpts); wsMap != nil {
|
||
path := getMapString(wsMap, "path")
|
||
// path 为 "/" 也要设置
|
||
if path != "" {
|
||
ws["path"] = path
|
||
}
|
||
if headers, ok := wsMap["headers"]; ok {
|
||
if headerMap := toStringMap(headers); headerMap != nil {
|
||
if hostH := getMapString(headerMap, "Host"); hostH != "" {
|
||
ws["headers"] = map[string]interface{}{"Host": hostH}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
stream["wsSettings"] = ws
|
||
}
|
||
if network == "grpc" {
|
||
stream["network"] = "grpc"
|
||
if grpcOpts, ok := node["grpc-opts"]; ok {
|
||
if grpcMap := toStringMap(grpcOpts); grpcMap != nil {
|
||
serviceName := getMapString(grpcMap, "grpc-service-name")
|
||
if serviceName != "" {
|
||
stream["grpcSettings"] = map[string]interface{}{"serviceName": serviceName}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if len(stream) > 0 {
|
||
out["streamSettings"] = stream
|
||
}
|
||
return out, "", nil
|
||
}
|
||
|
||
func buildOutboundFromClashVmess(node map[string]interface{}) (map[string]interface{}, string, error) {
|
||
host := getMapString(node, "server")
|
||
port := getMapInt(node, "port")
|
||
id := getMapString(node, "uuid")
|
||
cipher := getMapString(node, "cipher")
|
||
if cipher == "" {
|
||
cipher = "auto"
|
||
}
|
||
network := getMapString(node, "network")
|
||
// sni 和 servername 都要读
|
||
sni := getMapString(node, "sni")
|
||
if sni == "" {
|
||
sni = getMapString(node, "servername")
|
||
}
|
||
out := map[string]interface{}{
|
||
"protocol": "vmess",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"vnext": []interface{}{
|
||
map[string]interface{}{
|
||
"address": host,
|
||
"port": port,
|
||
"users": []interface{}{
|
||
map[string]interface{}{
|
||
"id": id,
|
||
"security": cipher,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
stream := map[string]interface{}{}
|
||
if getMapBool(node, "tls") || strings.ToLower(getMapString(node, "tls")) == "true" {
|
||
tlsSettings := map[string]interface{}{}
|
||
if sni != "" {
|
||
tlsSettings["serverName"] = sni
|
||
}
|
||
skipVerify := getMapBool(node, "skip-cert-verify")
|
||
tlsSettings["allowInsecure"] = skipVerify
|
||
stream["security"] = "tls"
|
||
stream["tlsSettings"] = tlsSettings
|
||
}
|
||
if network == "ws" {
|
||
stream["network"] = "ws"
|
||
ws := map[string]interface{}{}
|
||
if wsOpts, ok := node["ws-opts"]; ok {
|
||
if wsMap := toStringMap(wsOpts); wsMap != nil {
|
||
path := getMapString(wsMap, "path")
|
||
// path 为 "/" 也要设置
|
||
if path != "" {
|
||
ws["path"] = path
|
||
}
|
||
if headers, ok := wsMap["headers"]; ok {
|
||
if headerMap := toStringMap(headers); headerMap != nil {
|
||
if hostH := getMapString(headerMap, "Host"); hostH != "" {
|
||
ws["headers"] = map[string]interface{}{"Host": hostH}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
stream["wsSettings"] = ws
|
||
}
|
||
if network == "grpc" {
|
||
stream["network"] = "grpc"
|
||
if grpcOpts, ok := node["grpc-opts"]; ok {
|
||
if grpcMap := toStringMap(grpcOpts); grpcMap != nil {
|
||
serviceName := getMapString(grpcMap, "grpc-service-name")
|
||
if serviceName != "" {
|
||
stream["grpcSettings"] = map[string]interface{}{"serviceName": serviceName}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if len(stream) > 0 {
|
||
out["streamSettings"] = stream
|
||
}
|
||
return out, "", nil
|
||
}
|
||
|
||
func buildOutboundFromClashTrojan(node map[string]interface{}) (map[string]interface{}, string, error) {
|
||
host := getMapString(node, "server")
|
||
port := getMapInt(node, "port")
|
||
password := getMapString(node, "password")
|
||
sni := getMapString(node, "sni")
|
||
if sni == "" {
|
||
sni = getMapString(node, "servername")
|
||
}
|
||
network := getMapString(node, "network")
|
||
skipVerify := getMapBool(node, "skip-cert-verify")
|
||
|
||
out := map[string]interface{}{
|
||
"protocol": "trojan",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"address": host,
|
||
"port": port,
|
||
"password": password,
|
||
},
|
||
}
|
||
stream := map[string]interface{}{
|
||
"security": "tls",
|
||
"tlsSettings": map[string]interface{}{
|
||
"serverName": sni,
|
||
"allowInsecure": skipVerify,
|
||
},
|
||
}
|
||
if network == "ws" {
|
||
stream["network"] = "ws"
|
||
ws := map[string]interface{}{}
|
||
if wsOpts, ok := node["ws-opts"]; ok {
|
||
if wsMap := toStringMap(wsOpts); wsMap != nil {
|
||
if path := getMapString(wsMap, "path"); path != "" {
|
||
ws["path"] = path
|
||
}
|
||
if headers := toStringMap(wsMap["headers"]); headers != nil {
|
||
if h := getMapString(headers, "Host"); h != "" {
|
||
ws["headers"] = map[string]interface{}{"Host": h}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
stream["wsSettings"] = ws
|
||
} else if network == "grpc" {
|
||
stream["network"] = "grpc"
|
||
if grpcOpts, ok := node["grpc-opts"]; ok {
|
||
if grpcMap := toStringMap(grpcOpts); grpcMap != nil {
|
||
if svcName := getMapString(grpcMap, "grpc-service-name"); svcName != "" {
|
||
stream["grpcSettings"] = map[string]interface{}{"serviceName": svcName}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
out["streamSettings"] = stream
|
||
return out, "", nil
|
||
}
|
||
|
||
func buildOutboundFromClashHysteria2(node map[string]interface{}) (map[string]interface{}, string, error) {
|
||
// 支持的协议: vless, vmess, trojan, shadowsocks, socks, http, wireguard
|
||
// hysteria2 需要使用 Hysteria 客户端或 sing-box
|
||
return nil, "", fmt.Errorf("Xray 不支持 hysteria2 协议,请使用 vless/vmess/socks5/http 格式的代理")
|
||
}
|
||
|
||
func buildXrayOutbound(node string) (map[string]interface{}, error) {
|
||
l := strings.ToLower(node)
|
||
if strings.HasPrefix(l, "vmess://") {
|
||
return buildOutboundVmess(node)
|
||
}
|
||
if strings.HasPrefix(l, "vless://") {
|
||
return buildOutboundVless(node)
|
||
}
|
||
if strings.HasPrefix(l, "trojan://") {
|
||
return buildOutboundTrojan(node)
|
||
}
|
||
if strings.HasPrefix(l, "ss://") {
|
||
return buildOutboundSS(node)
|
||
}
|
||
if strings.HasPrefix(l, "ssr://") {
|
||
return nil, fmt.Errorf("不支持 ShadowsocksR 协议,Xray 不支持 SSR,请使用 SS/vmess/vless/trojan")
|
||
}
|
||
if strings.HasPrefix(l, "hysteria2://") || strings.HasPrefix(l, "hysteria://") {
|
||
return buildOutboundHysteria2(node)
|
||
}
|
||
return nil, fmt.Errorf("不支持的节点协议")
|
||
}
|
||
|
||
func buildOutboundVmess(node string) (map[string]interface{}, error) {
|
||
raw := strings.TrimPrefix(node, "vmess://")
|
||
decoded, err := decodeBase64String(strings.TrimSpace(raw))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("vmess 解析失败: %v", err)
|
||
}
|
||
var v struct {
|
||
Add string `json:"add"`
|
||
Port string `json:"port"`
|
||
ID string `json:"id"`
|
||
Net string `json:"net"`
|
||
Type string `json:"type"`
|
||
Host string `json:"host"`
|
||
Path string `json:"path"`
|
||
TLS string `json:"tls"`
|
||
Sni string `json:"sni"`
|
||
Alpn string `json:"alpn"`
|
||
}
|
||
if err := json.Unmarshal(decoded, &v); err != nil {
|
||
return nil, fmt.Errorf("vmess 配置解析失败: %v", err)
|
||
}
|
||
p, _ := strconv.Atoi(v.Port)
|
||
out := map[string]interface{}{
|
||
"protocol": "vmess",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"vnext": []interface{}{
|
||
map[string]interface{}{
|
||
"address": v.Add,
|
||
"port": p,
|
||
"users": []interface{}{
|
||
map[string]interface{}{
|
||
"id": v.ID,
|
||
"security": "auto",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
stream := map[string]interface{}{}
|
||
if v.TLS == "tls" {
|
||
stream["security"] = "tls"
|
||
if v.Sni != "" {
|
||
stream["tlsSettings"] = map[string]interface{}{"serverName": v.Sni}
|
||
}
|
||
}
|
||
if v.Net == "ws" {
|
||
stream["network"] = "ws"
|
||
ws := map[string]interface{}{}
|
||
if v.Path != "" {
|
||
ws["path"] = v.Path
|
||
}
|
||
if v.Host != "" {
|
||
ws["headers"] = map[string]interface{}{"Host": v.Host}
|
||
}
|
||
if len(ws) > 0 {
|
||
stream["wsSettings"] = ws
|
||
}
|
||
}
|
||
if len(stream) > 0 {
|
||
out["streamSettings"] = stream
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func buildOutboundVless(node string) (map[string]interface{}, error) {
|
||
u, err := url.Parse(node)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("vless 解析失败: %v", err)
|
||
}
|
||
host := u.Hostname()
|
||
portStr := u.Port()
|
||
p, _ := strconv.Atoi(portStr)
|
||
id := u.User.Username()
|
||
q := u.Query()
|
||
flow := q.Get("flow")
|
||
sec := strings.ToLower(q.Get("security"))
|
||
sni := q.Get("sni")
|
||
out := map[string]interface{}{
|
||
"protocol": "vless",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"vnext": []interface{}{
|
||
map[string]interface{}{
|
||
"address": host,
|
||
"port": p,
|
||
"users": []interface{}{
|
||
map[string]interface{}{
|
||
"id": id,
|
||
"flow": flow,
|
||
"encryption": "none",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
stream := map[string]interface{}{}
|
||
if sec == "tls" || sec == "reality" {
|
||
stream["security"] = "tls"
|
||
if sni != "" {
|
||
stream["tlsSettings"] = map[string]interface{}{"serverName": sni}
|
||
}
|
||
}
|
||
network := q.Get("type")
|
||
if network == "" {
|
||
network = q.Get("network")
|
||
}
|
||
if network == "ws" {
|
||
stream["network"] = "ws"
|
||
ws := map[string]interface{}{}
|
||
if pth := q.Get("path"); pth != "" {
|
||
ws["path"] = pth
|
||
}
|
||
hostH := q.Get("host")
|
||
if hostH == "" {
|
||
hostH = u.Hostname()
|
||
}
|
||
if hostH != "" {
|
||
ws["headers"] = map[string]interface{}{"Host": hostH}
|
||
}
|
||
stream["wsSettings"] = ws
|
||
}
|
||
if len(stream) > 0 {
|
||
out["streamSettings"] = stream
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func buildOutboundHysteria2(node string) (map[string]interface{}, error) {
|
||
// Xray 不支持 hysteria2 作为 outbound 协议
|
||
// 支持的协议: vless, vmess, trojan, shadowsocks, socks, http, wireguard
|
||
// hysteria2 需要使用 Hysteria 客户端或 sing-box
|
||
return nil, fmt.Errorf("Xray 不支持 hysteria2 协议,请使用 vless/vmess/socks5/http 格式的代理")
|
||
}
|
||
|
||
// buildOutboundTrojan 解析 trojan:// URI 格式
|
||
func buildOutboundTrojan(node string) (map[string]interface{}, error) {
|
||
u, err := url.Parse(node)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("trojan 解析失败: %v", err)
|
||
}
|
||
host := u.Hostname()
|
||
portStr := u.Port()
|
||
p, _ := strconv.Atoi(portStr)
|
||
password := u.User.Username()
|
||
q := u.Query()
|
||
sni := q.Get("sni")
|
||
if sni == "" {
|
||
sni = q.Get("peer")
|
||
}
|
||
skipVerify := q.Get("allowInsecure") == "1" || strings.ToLower(q.Get("allowInsecure")) == "true"
|
||
network := q.Get("type")
|
||
|
||
out := map[string]interface{}{
|
||
"protocol": "trojan",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"address": host,
|
||
"port": p,
|
||
"password": password,
|
||
},
|
||
}
|
||
stream := map[string]interface{}{
|
||
"security": "tls",
|
||
"tlsSettings": map[string]interface{}{
|
||
"serverName": sni,
|
||
"allowInsecure": skipVerify,
|
||
},
|
||
}
|
||
if network == "ws" {
|
||
stream["network"] = "ws"
|
||
ws := map[string]interface{}{}
|
||
if pth := q.Get("path"); pth != "" {
|
||
ws["path"] = pth
|
||
}
|
||
if h := q.Get("host"); h != "" {
|
||
ws["headers"] = map[string]interface{}{"Host": h}
|
||
}
|
||
stream["wsSettings"] = ws
|
||
}
|
||
out["streamSettings"] = stream
|
||
return out, nil
|
||
}
|
||
|
||
// buildOutboundFromClashSS 从 Clash YAML 格式解析 Shadowsocks outbound
|
||
func buildOutboundFromClashSS(node map[string]interface{}) (map[string]interface{}, string, error) {
|
||
host := getMapString(node, "server")
|
||
port := getMapInt(node, "port")
|
||
password := getMapString(node, "password")
|
||
cipher := getMapString(node, "cipher")
|
||
if cipher == "" {
|
||
cipher = getMapString(node, "method")
|
||
}
|
||
if cipher == "" {
|
||
cipher = "aes-256-gcm"
|
||
}
|
||
out := map[string]interface{}{
|
||
"protocol": "shadowsocks",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"address": host,
|
||
"port": port,
|
||
"method": cipher,
|
||
"password": password,
|
||
},
|
||
}
|
||
// plugin 支持(obfs/v2ray-plugin)
|
||
if plugin := getMapString(node, "plugin"); plugin != "" {
|
||
pluginOpts := getMapString(node, "plugin-opts")
|
||
_ = pluginOpts // xray 原生不支持 plugin,忽略
|
||
}
|
||
return out, "", nil
|
||
}
|
||
|
||
// buildOutboundSS 解析 ss:// URI 格式
|
||
// 支持两种格式:
|
||
// 1. ss://BASE64(method:password)@host:port
|
||
// 2. ss://BASE64(method:password@host:port)
|
||
func buildOutboundSS(node string) (map[string]interface{}, error) {
|
||
raw := strings.TrimPrefix(node, "ss://")
|
||
// 去掉 fragment(#备注)
|
||
if idx := strings.Index(raw, "#"); idx >= 0 {
|
||
raw = raw[:idx]
|
||
}
|
||
raw = strings.TrimSpace(raw)
|
||
|
||
var host, method, password string
|
||
var port int
|
||
|
||
// 格式1:method:password@host:port(SIP002)
|
||
if strings.Contains(raw, "@") {
|
||
u, err := url.Parse("ss://" + raw)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ss 解析失败: %v", err)
|
||
}
|
||
host = u.Hostname()
|
||
port, _ = strconv.Atoi(u.Port())
|
||
userInfo := u.User.String()
|
||
// userInfo 可能是 base64 编码的 method:password
|
||
if decoded, err := decodeBase64String(userInfo); err == nil {
|
||
parts := strings.SplitN(string(decoded), ":", 2)
|
||
if len(parts) == 2 {
|
||
method = parts[0]
|
||
password = parts[1]
|
||
}
|
||
} else {
|
||
// 明文 method:password
|
||
parts := strings.SplitN(userInfo, ":", 2)
|
||
if len(parts) == 2 {
|
||
method = parts[0]
|
||
password = parts[1]
|
||
}
|
||
}
|
||
} else {
|
||
// 格式2:整体 base64
|
||
decoded, err := decodeBase64String(raw)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ss base64 解析失败: %v", err)
|
||
}
|
||
// method:password@host:port
|
||
s := string(decoded)
|
||
atIdx := strings.LastIndex(s, "@")
|
||
if atIdx < 0 {
|
||
return nil, fmt.Errorf("ss 格式错误")
|
||
}
|
||
userPart := s[:atIdx]
|
||
hostPart := s[atIdx+1:]
|
||
parts := strings.SplitN(userPart, ":", 2)
|
||
if len(parts) == 2 {
|
||
method = parts[0]
|
||
password = parts[1]
|
||
}
|
||
hostPort := strings.Split(hostPart, ":")
|
||
if len(hostPort) == 2 {
|
||
host = hostPort[0]
|
||
port, _ = strconv.Atoi(hostPort[1])
|
||
}
|
||
}
|
||
|
||
if host == "" || port == 0 || method == "" {
|
||
return nil, fmt.Errorf("ss 节点信息不完整")
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"protocol": "shadowsocks",
|
||
"tag": "proxy-out",
|
||
"settings": map[string]interface{}{
|
||
"address": host,
|
||
"port": port,
|
||
"method": method,
|
||
"password": password,
|
||
},
|
||
}, nil
|
||
}
|