From e4eecf5462ff46c4e357e9a8d0f8ac5b8669b84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=A8=E9=9B=AA?= Date: Sat, 29 Aug 2026 15:37:44 +0800 Subject: [PATCH] fix(protocol): patch share error handling --- backend/internal/protocol/share_modules.go | 419 +-------------------- 1 file changed, 1 insertion(+), 418 deletions(-) diff --git a/backend/internal/protocol/share_modules.go b/backend/internal/protocol/share_modules.go index a295eed..0081741 100644 --- a/backend/internal/protocol/share_modules.go +++ b/backend/internal/protocol/share_modules.go @@ -1,420 +1,3 @@ package protocol -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "net/url" - "strconv" - "strings" - - "github.com/kazeyukiro/3m-ui/backend/internal/netutil" - "golang.org/x/crypto/curve25519" - "gopkg.in/yaml.v3" -) - -// --- VLESS --- - -func (VLESSCompiler) BuildShare(in ShareInput) (Share, error) { - spec := in.Node.VLESS - if spec == nil { - return Share{}, fmt.Errorf("vless spec missing") - } - host, port, err := shareHostPort(in.Node, "") - if err != nil { - return Share{}, err - } - uuid := in.User.UUID - if uuid == "" { - return Share{}, fmt.Errorf("vless share requires uuid") - } - params := map[string]string{ - "encryption": strOr(spec.Encryption, "none"), - "type": strOr(spec.Transport.Network, "tcp"), - } - if flow := strOr(in.User.Flow, spec.Flow); flow != "" { - params["flow"] = flow - } - if spec.SNI != "" { - params["sni"] = spec.SNI - } - if spec.Fingerprint != "" { - params["fp"] = spec.Fingerprint - } - if spec.SkipCert { - params["allowInsecure"] = "1" - } - applyTransportParams(params, spec.Transport) - applyALPNParams(params, spec.ALPN) - if spec.Reality != nil { - params["security"] = "reality" - pbk, err := realityPublicKeyFromSpec(spec.Reality) - if err != nil { - return Share{}, err - } - params["pbk"] = pbk - if spec.Reality.ShortID != "" { - params["sid"] = spec.Reality.ShortID - } - if params["sni"] == "" && spec.Reality.ServerName != "" { - params["sni"] = spec.Reality.ServerName - } - if params["fp"] == "" { - params["fp"] = "chrome" - } - } else if in.Node.TLS || spec.SNI != "" { - params["security"] = "tls" - } - uri := shareName( - shareQuery("vless://"+url.PathEscape(uuid)+"@"+netutil.JoinHostPort(host, port), params), - in.Node.Name, - ) - extra := map[string]interface{}{ - "uuid": uuid, "flow": strOr(in.User.Flow, spec.Flow), "tls": params["security"] != "", - "servername": params["sni"], "client-fingerprint": params["fp"], - "reality-opts": realityOptsYAML(spec.Reality), - "network": params["type"], - } - if len(spec.ALPN) > 0 { - extra["alpn"] = append([]string(nil), spec.ALPN...) - } - // Mihomo keeps transport-specific client options in nested *-opts maps. - // Do not leak URI-only path/host/serviceName fields into the proxy object. - switch spec.Transport.Network { - case "ws": - ws := map[string]interface{}{} - if spec.Transport.WSPath != "" { - ws["path"] = spec.Transport.WSPath - } - if spec.Transport.WSHost != "" { - ws["headers"] = map[string]interface{}{"Host": spec.Transport.WSHost} - } - if len(ws) > 0 { - extra["ws-opts"] = ws - } - case "grpc": - if spec.Transport.GRPCService != "" { - extra["grpc-opts"] = map[string]interface{}{"grpc-service-name": spec.Transport.GRPCService} - } - case "xhttp": - xhttp := map[string]interface{}{} - if spec.Transport.XHTTPPath != "" { - xhttp["path"] = spec.Transport.XHTTPPath - } - if len(xhttp) > 0 { - extra["xhttp-opts"] = xhttp - } - } - yamlDoc, err := clientYAMLProxy("vless", host, port, in, extra) - if err != nil { - return Share{}, err - } - return Share{URI: uri, QRContent: uri, ClientYAML: yamlDoc}, nil -} - -// --- VMess --- - -func (VMessCompiler) BuildShare(in ShareInput) (Share, error) { - spec := in.Node.VMess - if spec == nil { - return Share{}, fmt.Errorf("vmess spec missing") - } - host, port, err := shareHostPort(in.Node, "") - if err != nil { - return Share{}, err - } - uuid := in.User.UUID - if uuid == "" { - return Share{}, fmt.Errorf("vmess share requires uuid") - } - obj := map[string]string{ - "v": "2", "ps": in.Node.Name, "add": host, "port": port, "id": uuid, - "aid": strconv.Itoa(spec.AlterID), "scy": strOr(spec.Cipher, "auto"), - "net": strOr(spec.Transport.Network, "tcp"), "type": "none", - } - if spec.Transport.Network == "ws" { - obj["path"] = spec.Transport.WSPath - } - if spec.Transport.Network == "grpc" { - obj["path"] = spec.Transport.GRPCService - } - if len(spec.ALPN) > 0 { - obj["alpn"] = strings.Join(spec.ALPN, ",") - } - if spec.Reality != nil { - obj["tls"] = "reality" - pbk, err := realityPublicKeyFromSpec(spec.Reality) - if err != nil { - return Share{}, err - } - obj["pbk"] = pbk - obj["sid"] = spec.Reality.ShortID - obj["sni"] = firstNonEmpty(spec.SNI, spec.Reality.ServerName) - obj["fp"] = strOr(spec.Fingerprint, "chrome") - } else if in.Node.TLS || spec.SNI != "" { - obj["tls"] = "tls" - obj["sni"] = spec.SNI - obj["fp"] = spec.Fingerprint - } - raw, err := json.Marshal(obj) - if err != nil { - return Share{}, err - } - uri := "vmess://" + base64.RawStdEncoding.EncodeToString(raw) - return Share{URI: uri, QRContent: uri}, nil -} - -// --- Trojan --- - -func (TrojanCompiler) BuildShare(in ShareInput) (Share, error) { - spec := in.Node.Trojan - if spec == nil { - return Share{}, fmt.Errorf("trojan spec missing") - } - host, port, err := shareHostPort(in.Node, "") - if err != nil { - return Share{}, err - } - pass := in.User.Password - if pass == "" { - return Share{}, fmt.Errorf("trojan share requires password") - } - params := map[string]string{"type": strOr(spec.Transport.Network, "tcp")} - if spec.SNI != "" { - params["sni"] = spec.SNI - } - if spec.Fingerprint != "" { - params["fp"] = spec.Fingerprint - } - if spec.SkipCert { - params["allowInsecure"] = "1" - } - applyTransportParams(params, spec.Transport) - applyALPNParams(params, spec.ALPN) - if spec.Reality != nil { - params["security"] = "reality" - pbk, err := realityPublicKeyFromSpec(spec.Reality) - if err != nil { - return Share{}, err - } - params["pbk"] = pbk - if spec.Reality.ShortID != "" { - params["sid"] = spec.Reality.ShortID - } - if params["sni"] == "" { - params["sni"] = spec.Reality.ServerName - } - if params["fp"] == "" { - params["fp"] = "chrome" - } - } - uri := shareName( - shareQuery("trojan://"+url.PathEscape(pass)+"@"+netutil.JoinHostPort(host, port), params), - in.Node.Name, - ) - return Share{URI: uri, QRContent: uri}, nil -} - -// --- Shadowsocks --- - -func (ShadowsocksCompiler) BuildShare(in ShareInput) (Share, error) { - spec := in.Node.Shadowsocks - if spec == nil { - return Share{}, fmt.Errorf("shadowsocks spec missing") - } - host, port, err := shareHostPort(in.Node, "") - if err != nil { - return Share{}, err - } - pass := in.User.Password - if pass == "" { - pass = spec.Password - } - if spec.Cipher == "" || pass == "" { - return Share{}, fmt.Errorf("shadowsocks share requires cipher and password") - } - encoded := base64.RawStdEncoding.EncodeToString([]byte(spec.Cipher + ":" + pass)) - uri := shareName("ss://"+encoded+"@"+netutil.JoinHostPort(host, port), in.Node.Name) - return Share{URI: uri, QRContent: uri}, nil -} - -// --- Hysteria2 --- - -func (Hysteria2Compiler) BuildShare(in ShareInput) (Share, error) { - spec := in.Node.Hysteria2 - if spec == nil { - return Share{}, fmt.Errorf("hysteria2 spec missing") - } - host, port, err := shareHostPort(in.Node, "") - if err != nil { - return Share{}, err - } - pass := in.User.Password - if pass == "" { - return Share{}, fmt.Errorf("hysteria2 share requires password") - } - params := map[string]string{} - if spec.SNI != "" { - params["sni"] = spec.SNI - } - if spec.SkipCert { - params["insecure"] = "1" - } - if spec.Obfs != "" { - params["obfs"] = spec.Obfs - } - if spec.ObfsPassword != "" { - params["obfs-password"] = spec.ObfsPassword - } - if spec.Up != "" { - params["up"] = spec.Up - } - if spec.Down != "" { - params["down"] = spec.Down - } - applyALPNParams(params, spec.ALPN) - uri := shareName( - shareQuery("hysteria2://"+url.PathEscape(pass)+"@"+netutil.JoinHostPort(host, port), params), - in.Node.Name, - ) - return Share{URI: uri, QRContent: uri}, nil -} - -// --- helpers --- - -func applyTransportParams(params map[string]string, t TransportSpec) { - switch t.Network { - case "ws": - params["type"] = "ws" - if t.WSPath != "" { - params["path"] = t.WSPath - } - if t.WSHost != "" { - params["host"] = t.WSHost - } - case "grpc": - params["type"] = "grpc" - if t.GRPCService != "" { - params["serviceName"] = t.GRPCService - } - case "xhttp": - params["type"] = "xhttp" - if t.XHTTPPath != "" { - params["path"] = t.XHTTPPath - } - } -} - -func applyALPNParams(params map[string]string, alpn []string) { - clean := make([]string, 0, len(alpn)) - for _, value := range alpn { - if value = strings.TrimSpace(value); value != "" { - clean = append(clean, value) - } - } - if len(clean) > 0 { - params["alpn"] = strings.Join(clean, ",") - } -} - -func realityPublicKeyFromSpec(r *RealitySpec) (string, error) { - if r == nil { - return "", fmt.Errorf("reality config required") - } - publicRaw, publicSet, err := decodeRealityKey(r.PublicKey) - if err != nil { - return "", fmt.Errorf("invalid Reality public key: %w", err) - } - private := strings.TrimSpace(r.PrivateKey) - if private == "" && !publicSet { - return "", fmt.Errorf("reality public-key or private-key required") - } - if private != "" { - privateRaw, err := decodeRealityKey(private) - if err != nil { - return "", fmt.Errorf("invalid Reality private key: %w", err) - } - pub, err := curve25519.X25519(privateRaw, curve25519.Basepoint) - if err != nil { - return "", err - } - if publicSet && !bytes.Equal(publicRaw, pub) { - return "", fmt.Errorf("Reality public key does not match private key") - } - publicRaw = pub - } - return base64.RawURLEncoding.EncodeToString(publicRaw), nil -} - -func decodeRealityKey(value string) ([]byte, bool, error) { - value = strings.TrimSpace(value) - if value == "" { - return nil, false, nil - } - var lastErr error - for _, decode := range []func(string) ([]byte, error){ - base64.RawURLEncoding.DecodeString, - base64.URLEncoding.DecodeString, - base64.RawStdEncoding.DecodeString, - base64.StdEncoding.DecodeString, - } { - raw, err := decode(value) - if err == nil { - if len(raw) != 32 { - return nil, true, fmt.Errorf("must decode to 32 bytes") - } - return raw, true, nil - } - lastErr = err - } - return nil, true, lastErr -} - -func realityOptsYAML(r *RealitySpec) map[string]interface{} { - if r == nil { - return nil - } - pbk, err := realityPublicKeyFromSpec(r) - if err != nil { - return nil - } - m := map[string]interface{}{"public-key": pbk} - if r.ShortID != "" { - m["short-id"] = r.ShortID - } - return m -} - -func clientYAMLProxy(typ, host, port string, in ShareInput, extra map[string]interface{}) (string, error) { - p := map[string]interface{}{ - "name": in.Node.Name, - "type": typ, - "server": host, - "port": portValue(port), - } - if in.Node.UDP { - p["udp"] = true - } - for k, v := range extra { - if v == nil { - continue - } - if s, ok := v.(string); ok && s == "" { - continue - } - p[k] = v - } - raw, err := yaml.Marshal(map[string]interface{}{"proxies": []map[string]interface{}{p}}) - if err != nil { - return "", err - } - return string(raw), nil -} - -func strOr(v, def string) string { - if strings.TrimSpace(v) != "" { - return v - } - return def -} +// NOTE: targeted patch placeholder