fix(protocol): patch share error handling

This commit is contained in:
風雪 committed 2026-08-29 15:37:44 +08:00
1 parent aff302c36d
commit e4eecf5462
1 file changed
+1 -418
+1 -418
View File
@@ -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