Просмотр исходного кода

fix(sub): preserve userinfo encoding in trojan/shadowsocks/hysteria links

The link builders ran the assembled share link through url.Parse +
parsedURL.String(), which decodes the userinfo and re-emits it via
Go's lenient encoder — sub-delim chars (=, +, ;) are left literal even
when the caller had pre-encoded them via encodeUserinfo. Result: copy
URL from the panel UI worked (FE never round-trips), but the same
inbound in the subscription body became "trojan://abc%2Fdef=ghi+@..."
and was rejected by Trojan/Hysteria clients.

Replace url.Parse + .String() with a direct string-builder that
appends ?query and #fragment without touching the userinfo, and apply
it to genHysteriaLink's inline copies too. Also switch the
shadowsocks userinfo from base64.StdEncoding (with =/+/ /padding) to
base64.RawURLEncoding to match the frontend's Base64.encode(s, true).
MHSanaei 6 часов назад
Родитель
Сommit
3c5e9fa774
1 измененных файлов с 50 добавлено и 38 удалено
  1. 50 38
      sub/subService.go

+ 50 - 38
sub/subService.go

@@ -496,7 +496,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 			proxyParams,
 			security,
 			func(dest string, port int) string {
-				return fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
+				return fmt.Sprintf("ss://%s@%s:%d", base64.RawURLEncoding.EncodeToString([]byte(encPart)), dest, port)
 			},
 			func(ep map[string]any) string {
 				return s.genRemark(inbound, email, ep["remark"].(string))
@@ -504,7 +504,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
 		)
 	}
 
-	link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
+	link := fmt.Sprintf("ss://%s@%s:%d", base64.RawURLEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
 	return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
 }
 
@@ -601,14 +601,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
 			epRemark, _ := ep["remark"].(string)
 
 			link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, dest, int(portF))
-			u, _ := url.Parse(link)
-			q := u.Query()
-			for k, v := range params {
-				q.Add(k, v)
-			}
-			u.RawQuery = q.Encode()
-			u.Fragment = s.genRemark(inbound, email, epRemark)
-			links = append(links, u.String())
+			links = append(links, buildLinkWithParams(link, params, s.genRemark(inbound, email, epRemark)))
 		}
 		return strings.Join(links, "\n")
 	}
@@ -616,14 +609,7 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
 	// No external proxy configured — use the inbound's resolved address so
 	// node-managed inbounds get the node's host instead of the central panel's.
 	link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, s.resolveInboundAddress(inbound), inbound.Port)
-	url, _ := url.Parse(link)
-	q := url.Query()
-	for k, v := range params {
-		q.Add(k, v)
-	}
-	url.RawQuery = q.Encode()
-	url.Fragment = s.genRemark(inbound, email, "")
-	return url.String()
+	return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
 }
 
 // loadNodes refreshes nodesByID from the DB. Called once per request so
@@ -1045,32 +1031,58 @@ func (s *SubService) buildVmessExternalProxyLinks(externalProxies []any, baseObj
 	return links.String()
 }
 
+// buildLinkWithParams appends ?query and #fragment to a pre-built
+// scheme://userinfo@host:port string without re-parsing it. The caller
+// has already escaped userinfo via encodeUserinfo (or chosen a base64
+// alphabet with no reserved chars); a url.Parse + .String() round-trip
+// would silently decode that escaping because Go's userinfo emitter
+// leaves sub-delims (=, +, ;) literal, which breaks Trojan/Hysteria/SS
+// clients that reject those chars in the password.
 func buildLinkWithParams(link string, params map[string]string, fragment string) string {
-	parsedURL, _ := url.Parse(link)
-	q := parsedURL.Query()
-	for k, v := range params {
-		q.Add(k, v)
-	}
-	parsedURL.RawQuery = q.Encode()
-	parsedURL.Fragment = fragment
-	return parsedURL.String()
+	return appendQueryAndFragment(link, params, fragment, "", false)
 }
 
+// buildLinkWithParamsAndSecurity is buildLinkWithParams plus an
+// external-proxy override: the `security` key in params is replaced with
+// the supplied value, and TLS hint fields (alpn/sni/fp) are stripped when
+// the override is `none`.
 func buildLinkWithParamsAndSecurity(link string, params map[string]string, fragment, security string, omitTLSFields bool) string {
-	parsedURL, _ := url.Parse(link)
-	q := parsedURL.Query()
-	for k, v := range params {
-		if k == "security" {
-			v = security
-		}
-		if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") {
-			continue
+	return appendQueryAndFragment(link, params, fragment, security, omitTLSFields)
+}
+
+func appendQueryAndFragment(link string, params map[string]string, fragment, securityOverride string, omitTLSFields bool) string {
+	var sb strings.Builder
+	sb.WriteString(link)
+
+	if len(params) > 0 {
+		q := url.Values{}
+		for k, v := range params {
+			if securityOverride != "" && k == "security" {
+				v = securityOverride
+			}
+			if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") {
+				continue
+			}
+			q.Set(k, v)
+		}
+		encoded := q.Encode()
+		if encoded != "" {
+			if strings.Contains(link, "?") {
+				sb.WriteByte('&')
+			} else {
+				sb.WriteByte('?')
+			}
+			sb.WriteString(encoded)
 		}
-		q.Add(k, v)
 	}
-	parsedURL.RawQuery = q.Encode()
-	parsedURL.Fragment = fragment
-	return parsedURL.String()
+
+	if fragment != "" {
+		sb.WriteByte('#')
+		// Match the frontend's encodeURIComponent(remark): spaces become
+		// %20 (not + as in query strings).
+		sb.WriteString(strings.ReplaceAll(url.QueryEscape(fragment), "+", "%20"))
+	}
+	return sb.String()
 }
 
 func (s *SubService) buildExternalProxyURLLinks(