mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(sub): escape a VLESS id the way the panel's own links do
Subscription links escaped the VLESS id with url.QueryEscape, while the panel builds its own link and QR through the browser's URL parser, which escapes userinfo with the WHATWG userinfo set. A non-UUID id such as team+1 was therefore served as team%2B1 by the subscription and as team+1 in the QR, and a client that does not percent-decode userinfo authenticated with a different id depending on where it came from. The id is now escaped with exactly that set, which still keeps a line break or an "@" from splitting the link.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -231,3 +232,25 @@ func TestGenVlessLinkEscapesID(t *testing.T) {
|
||||
t.Fatalf("vless id not escaped: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The panel builds its own link and QR through the browser's URL parser, which escapes
|
||||
// userinfo with the WHATWG userinfo set; a subscription escaping more serves another id.
|
||||
func TestGenVlessLinkEscapesIDLikeThePanel(t *testing.T) {
|
||||
for id, want := range map[string]string{
|
||||
"team+1": "team+1",
|
||||
"a=b": "a%3Db",
|
||||
"x:y;z": "x%3Ay%3Bz",
|
||||
"ok!$,'()*~": "ok!$,'()*~",
|
||||
"é": "%C3%A9",
|
||||
} {
|
||||
in := &model.Inbound{
|
||||
Listen: "203.0.113.5", Port: 443, Protocol: model.VLESS, Remark: "n",
|
||||
Settings: `{"clients":[{"id":` + strconv.Quote(id) + `,"email":"user"}],"decryption":"none"}`,
|
||||
StreamSettings: `{"network":"tcp","security":"none"}`,
|
||||
}
|
||||
got := NewSubService("").genVlessLink(in, "user")
|
||||
if !strings.HasPrefix(got, "vless://"+want+"@203.0.113.5:443?") {
|
||||
t.Errorf("id %q: link %q, want userinfo %q", id, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -1432,7 +1432,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
params,
|
||||
security,
|
||||
func(ep map[string]any, dest string, port int) string {
|
||||
return fmt.Sprintf("vless://%s@%s", encodeUserinfo(applyVlessRoute(uuid, hostVlessRoute(ep))), joinHostPort(dest, port))
|
||||
return fmt.Sprintf("vless://%s@%s", encodeLinkUserinfo(applyVlessRoute(uuid, hostVlessRoute(ep))), joinHostPort(dest, port))
|
||||
},
|
||||
func(ep map[string]any) string {
|
||||
return s.endpointRemark(inbound, email, ep, streamNetwork)
|
||||
@@ -1440,7 +1440,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
)
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("vless://%s@%s", encodeUserinfo(uuid), joinHostPort(address, port))
|
||||
link := fmt.Sprintf("vless://%s@%s", encodeLinkUserinfo(uuid), joinHostPort(address, port))
|
||||
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
|
||||
}
|
||||
|
||||
@@ -1508,6 +1508,21 @@ func encodeUserinfo(s string) string {
|
||||
return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
|
||||
}
|
||||
|
||||
// encodeLinkUserinfo escapes with the WHATWG userinfo set, as the panel's own links do through
|
||||
// the browser's URL parser, so a non-UUID id reads the same in a subscription and a QR.
|
||||
func encodeLinkUserinfo(s string) string {
|
||||
const escaped = " \"#<>?`{}/:;=@[\\]^|"
|
||||
var sb strings.Builder
|
||||
for _, b := range []byte(s) {
|
||||
if b < 0x20 || b > 0x7e || strings.IndexByte(escaped, b) >= 0 {
|
||||
fmt.Fprintf(&sb, "%%%02X", b)
|
||||
} else {
|
||||
sb.WriteByte(b)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// joinHostPort wraps an IPv6 host in square brackets the way RFC 3986
|
||||
// requires for URI authorities, while leaving IPv4 addresses and hostnames
|
||||
// untouched. It also strips any brackets already present on the input so
|
||||
|
||||
Reference in New Issue
Block a user