mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-29 16:39:35 +00:00
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).
This commit is contained in:
@@ -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
|
||||
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)
|
||||
}
|
||||
if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") {
|
||||
continue
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user