mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 18:24:10 +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:
parent
31d7ed5103
commit
3c5e9fa774
1 changed files with 49 additions and 37 deletions
|
|
@ -496,7 +496,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
proxyParams,
|
proxyParams,
|
||||||
security,
|
security,
|
||||||
func(dest string, port int) string {
|
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 {
|
func(ep map[string]any) string {
|
||||||
return s.genRemark(inbound, email, ep["remark"].(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, ""))
|
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)
|
epRemark, _ := ep["remark"].(string)
|
||||||
|
|
||||||
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, dest, int(portF))
|
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, dest, int(portF))
|
||||||
u, _ := url.Parse(link)
|
links = append(links, buildLinkWithParams(link, params, s.genRemark(inbound, email, epRemark)))
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
return strings.Join(links, "\n")
|
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
|
// 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.
|
// 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)
|
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, s.resolveInboundAddress(inbound), inbound.Port)
|
||||||
url, _ := url.Parse(link)
|
return buildLinkWithParams(link, params, s.genRemark(inbound, email, ""))
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadNodes refreshes nodesByID from the DB. Called once per request so
|
// 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()
|
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 {
|
func buildLinkWithParams(link string, params map[string]string, fragment string) string {
|
||||||
parsedURL, _ := url.Parse(link)
|
return appendQueryAndFragment(link, params, fragment, "", false)
|
||||||
q := parsedURL.Query()
|
|
||||||
for k, v := range params {
|
|
||||||
q.Add(k, v)
|
|
||||||
}
|
|
||||||
parsedURL.RawQuery = q.Encode()
|
|
||||||
parsedURL.Fragment = fragment
|
|
||||||
return parsedURL.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func buildLinkWithParamsAndSecurity(link string, params map[string]string, fragment, security string, omitTLSFields bool) string {
|
||||||
parsedURL, _ := url.Parse(link)
|
return appendQueryAndFragment(link, params, fragment, security, omitTLSFields)
|
||||||
q := parsedURL.Query()
|
}
|
||||||
|
|
||||||
|
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 {
|
for k, v := range params {
|
||||||
if k == "security" {
|
if securityOverride != "" && k == "security" {
|
||||||
v = security
|
v = securityOverride
|
||||||
}
|
}
|
||||||
if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") {
|
if omitTLSFields && (k == "alpn" || k == "sni" || k == "fp") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
q.Add(k, v)
|
q.Set(k, v)
|
||||||
}
|
}
|
||||||
parsedURL.RawQuery = q.Encode()
|
encoded := q.Encode()
|
||||||
parsedURL.Fragment = fragment
|
if encoded != "" {
|
||||||
return parsedURL.String()
|
if strings.Contains(link, "?") {
|
||||||
|
sb.WriteByte('&')
|
||||||
|
} else {
|
||||||
|
sb.WriteByte('?')
|
||||||
|
}
|
||||||
|
sb.WriteString(encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
func (s *SubService) buildExternalProxyURLLinks(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue