fix(sub): SS2022 share links must not base64-encode userinfo (#5432)

Per SIP022, ss:// links for 2022-blake3-* methods must NOT base64-encode
the userinfo; method and password are percent-encoded instead. Clients
like Hiddify reject the base64 form. Fix both the server-side
subscription path and the client-side panel link, plus the matching
parsers for round-trip import.
This commit is contained in:
MHSanaei
2026-06-20 11:25:12 +02:00
parent c58db81da0
commit a5bc71a6f1
6 changed files with 43 additions and 11 deletions
+13
View File
@@ -617,6 +617,19 @@ export function genShadowsocksLink(input: GenShadowsocksLinkInput): string {
if (isSS2022) passwords.push(settings.password);
if (isSSMultiUser) passwords.push(clientPassword);
if (isSS2022) {
// SIP022 (2022-blake3-*) forbids base64 userinfo: method and each key are
// percent-encoded, joined by literal ':' separators. Built by hand because
// `new URL` would re-encode the inner key separator to %3A.
const userinfo = [settings.method, ...passwords].map(encodeURIComponent).join(':');
let link = `ss://${userinfo}@${formatUrlHost(address)}:${port}`;
const query = params.toString();
if (query) link += `?${query}`;
link += `#${encodeURIComponent(remark)}`;
return link;
}
// SIP002 userinfo is base64(method:pw).
const userinfo = Base64.encode(`${settings.method}:${passwords.join(':')}`, true);
const url = new URL(`ss://${userinfo}@${formatUrlHost(address)}:${port}`);
for (const [key, value] of params) url.searchParams.set(key, value);
@@ -372,8 +372,15 @@ export function parseShadowsocksLink(link: string): Raw | null {
const core = queryIndex >= 0 ? linkNoHash.slice(0, queryIndex) : linkNoHash;
const atIndex = core.indexOf('@');
if (atIndex >= 0) {
try { userInfo = Base64.decode(core.slice('ss://'.length, atIndex)); }
catch { userInfo = core.slice('ss://'.length, atIndex); }
const rawUserInfo = core.slice('ss://'.length, atIndex);
if (rawUserInfo.includes(':')) {
// SIP022 (2022-blake3-*) userinfo is percent-encoded, never base64
// (a literal ':' can't appear in a base64/base64url string).
try { userInfo = decodeURIComponent(rawUserInfo); } catch { userInfo = rawUserInfo; }
} else {
try { userInfo = Base64.decode(rawUserInfo); }
catch { userInfo = rawUserInfo; }
}
const hostPort = core.slice(atIndex + 1);
const colon = hostPort.lastIndexOf(':');
if (colon < 0) return null;