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:
MHSanaei
2026-11-01 10:26:54 +01:00
parent 0394996a30
commit 255c658468
2 changed files with 40 additions and 2 deletions
+23
View File
@@ -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)
}
}
}