fix(protocol): patch share error handling
This commit is contained in:
1 parent
aff302c36d
commit
e4eecf5462
1 file changed
+1
-418
@@ -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
|
||||
Reference in new issue
Block a user