Skip to content

Commit 48cdcad

Browse files
committed
feat: 支持非标用法: VMess Base64 URI 末尾加 #节点名
1 parent dd01117 commit 48cdcad

4 files changed

Lines changed: 182 additions & 11 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.21.93",
3+
"version": "2.21.94",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

backend/src/core/proxy-utils/parsers/index.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ function surge_port_hopping(raw) {
3232
};
3333
}
3434

35+
function splitURIFragment(raw) {
36+
const [__, content, fragment] = /^(.*?)(?:#(.*?))?$/.exec(raw);
37+
return {
38+
content,
39+
fragment:
40+
fragment != null ? decodeURIComponent(fragment) : undefined,
41+
};
42+
}
43+
3544
function URI_PROXY() {
3645
// socks5+tls
3746
// socks5
@@ -129,13 +138,12 @@ function URI_SS() {
129138
};
130139
const parse = (line) => {
131140
// parse url
132-
let content = line.split('ss://')[1];
133-
134-
let name = line.split('#')[1];
141+
let { content, fragment: name } = splitURIFragment(
142+
line.split('ss://')[1],
143+
);
135144
const proxy = {
136145
type: 'ss',
137146
};
138-
content = content.split('#')[0]; // strip proxy name
139147
// handle IPV4 and IPV6
140148
let serverAndPortArray = content.match(/@([^/?]*)(\/|\?|$)/);
141149

@@ -365,9 +373,6 @@ function URI_SS() {
365373
if (/(&|\?)tfo=(1|true)/i.test(query)) {
366374
proxy.tfo = true;
367375
}
368-
if (name != null) {
369-
name = decodeURIComponent(name);
370-
}
371376
proxy.name = name ?? `SS ${proxy.server}:${proxy.port}`;
372377
return proxy;
373378
};
@@ -450,8 +455,9 @@ function URI_VMess() {
450455
return /^vmess:\/\//.test(line);
451456
};
452457
const parse = (line) => {
453-
line = line.split('vmess://')[1];
454-
let content = Base64.decode(line.replace(/\?.*?$/, ''));
458+
let { content: lineWithoutFragment, fragment: fragmentName } =
459+
splitURIFragment(line.split('vmess://')[1]);
460+
let content = Base64.decode(lineWithoutFragment.replace(/\?.*?$/, ''));
455461
if (/=\s*vmess/.test(content)) {
456462
// Quantumult VMess URI format
457463
const partitions = content.split(',').map((p) => p.trim());
@@ -501,6 +507,9 @@ function URI_VMess() {
501507
throw new Error(`Unsupported obfs: ${params.obfs}`);
502508
}
503509
}
510+
if (isNotBlank(fragmentName)) {
511+
proxy.name = fragmentName;
512+
}
504513
return proxy;
505514
} else {
506515
let params = {};
@@ -511,7 +520,9 @@ function URI_VMess() {
511520
} catch (e) {
512521
// Shadowrocket URI format
513522
// eslint-disable-next-line no-unused-vars
514-
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(line);
523+
let [__, base64Line, qs] = /(^[^?]+?)\/?\?(.*)$/.exec(
524+
lineWithoutFragment,
525+
);
515526
content = Base64.decode(base64Line);
516527

517528
for (const addon of qs.split('&')) {
@@ -680,6 +691,9 @@ function URI_VMess() {
680691
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
681692
// 然而 wiki 和 app 实测中都没有字段表示这个
682693
// proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
694+
if (isNotBlank(fragmentName)) {
695+
proxy.name = fragmentName;
696+
}
683697

684698
return proxy;
685699
}

backend/src/test/proxy-parsers/uri.spec.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ describe('Proxy URI parser coverage', function () {
2222
});
2323
});
2424

25+
it('keeps the full https fragment comment after the first hash', function () {
26+
const proxy = parseOne(
27+
'https://alice:pa%24%24@https.example.com#HTTPS%20Outer#Remark',
28+
);
29+
30+
expectSubset(proxy, {
31+
type: 'http',
32+
name: 'HTTPS Outer#Remark',
33+
server: 'https.example.com',
34+
port: 443,
35+
tls: true,
36+
username: 'alice',
37+
password: 'pa$$',
38+
});
39+
});
40+
2541
it('parses legacy socks:// URIs with base64 auth', function () {
2642
const proxy = parseOne(
2743
`socks://${encodeURIComponent(
@@ -38,6 +54,23 @@ describe('Proxy URI parser coverage', function () {
3854
password: 'secret',
3955
});
4056
});
57+
58+
it('keeps the full socks fragment comment after the first hash', function () {
59+
const proxy = parseOne(
60+
`socks://${encodeURIComponent(
61+
Base64.encode('bob:secret'),
62+
)}@socks.example.com:1080#SOCKS#Remark`,
63+
);
64+
65+
expectSubset(proxy, {
66+
type: 'socks5',
67+
name: 'SOCKS#Remark',
68+
server: 'socks.example.com',
69+
port: 1080,
70+
username: 'bob',
71+
password: 'secret',
72+
});
73+
});
4174
});
4275

4376
describe('shadowsocks family', function () {
@@ -97,6 +130,24 @@ describe('Proxy URI parser coverage', function () {
97130
});
98131
});
99132

133+
it('keeps the full shadowsocks fragment comment after the first hash', function () {
134+
const userInfo = encodeURIComponent(
135+
Base64.encode('aes-128-gcm:secret'),
136+
);
137+
const proxy = parseOne(
138+
`ss://${userInfo}@ss.example.com:8388#SS%20Outer#Remark`,
139+
);
140+
141+
expectSubset(proxy, {
142+
type: 'ss',
143+
name: 'SS Outer#Remark',
144+
server: 'ss.example.com',
145+
port: 8388,
146+
cipher: 'aes-128-gcm',
147+
password: 'secret',
148+
});
149+
});
150+
100151
it('parses shadowsocks v2ray-plugin URI flags for sni, skip-cert-verify, and mux', function () {
101152
const userInfo = encodeURIComponent(
102153
Base64.encode('aes-128-gcm:secret'),

backend/src/test/proxy-parsers/v2ray-and-platforms.spec.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,84 @@ describe('VMess and VLESS parser coverage', function () {
6969
});
7070
});
7171

72+
it('prefers fragment comments over internal V2rayN vmess names', function () {
73+
const share = Base64.encode(
74+
JSON.stringify({
75+
v: '2',
76+
ps: 'VMess WS Fragment',
77+
add: 'vmess-fragment.example.com',
78+
port: '443',
79+
id: UUID,
80+
aid: '0',
81+
scy: 'none',
82+
net: 'ws',
83+
host: 'fragment.example.com',
84+
path: '/fragment?ed=2560',
85+
tls: 'tls',
86+
sni: 'sni.fragment.example.com',
87+
fp: 'chrome',
88+
}),
89+
);
90+
const proxy = parseOne(`vmess://${share}#Outer%20Fragment%20Remark`);
91+
92+
expectSubset(proxy, {
93+
type: 'vmess',
94+
name: 'Outer Fragment Remark',
95+
server: 'vmess-fragment.example.com',
96+
port: 443,
97+
uuid: UUID,
98+
alterId: 0,
99+
cipher: 'none',
100+
tls: true,
101+
sni: 'sni.fragment.example.com',
102+
'client-fingerprint': 'chrome',
103+
network: 'ws',
104+
'ws-opts': {
105+
path: '/fragment?ed=2560',
106+
headers: {
107+
Host: 'fragment.example.com',
108+
},
109+
},
110+
});
111+
});
112+
113+
it('keeps the full vmess fragment comment after the first hash', function () {
114+
const share = Base64.encode(
115+
JSON.stringify({
116+
v: '2',
117+
ps: 'VMess WS Fragment Full',
118+
add: 'vmess-fragment-full.example.com',
119+
port: '443',
120+
id: UUID,
121+
aid: '0',
122+
scy: 'auto',
123+
net: 'ws',
124+
host: 'fragment-full.example.com',
125+
path: '/fragment-full',
126+
tls: 'tls',
127+
}),
128+
);
129+
const proxy = parseOne(`vmess://${share}#Outer%20Fragment#Remark`);
130+
131+
expectSubset(proxy, {
132+
type: 'vmess',
133+
name: 'Outer Fragment#Remark',
134+
server: 'vmess-fragment-full.example.com',
135+
port: 443,
136+
uuid: UUID,
137+
alterId: 0,
138+
cipher: 'auto',
139+
tls: true,
140+
network: 'ws',
141+
'ws-opts': {
142+
path: '/fragment-full',
143+
headers: {
144+
Host: 'fragment-full.example.com',
145+
},
146+
},
147+
});
148+
});
149+
72150
it('parses V2rayN http shares and normalizes http paths', function () {
73151
const share = Base64.encode(
74152
JSON.stringify({
@@ -257,6 +335,34 @@ describe('VMess and VLESS parser coverage', function () {
257335
});
258336
});
259337

338+
it('keeps the full vless fragment comment after the first hash', function () {
339+
const proxy = parseOne(
340+
`vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&sni=sni.example.com&host=cdn.example.com&path=%2Fws&allowInsecure=1&fp=chrome&alpn=h2#VLESS%20Outer#Remark`,
341+
);
342+
343+
expectSubset(proxy, {
344+
type: 'vless',
345+
name: 'VLESS Outer#Remark',
346+
server: 'vless-ws.example.com',
347+
port: 443,
348+
uuid: UUID,
349+
tls: true,
350+
sni: 'sni.example.com',
351+
'skip-cert-verify': true,
352+
'client-fingerprint': 'chrome',
353+
alpn: ['h2'],
354+
udp: true,
355+
xudp: true,
356+
network: 'ws',
357+
'ws-opts': {
358+
path: '/ws',
359+
headers: {
360+
Host: 'cdn.example.com',
361+
},
362+
},
363+
});
364+
});
365+
260366
it('parses websocket VLESS shares with packet encoding and early data', function () {
261367
const proxy = parseOne(
262368
`vless://${UUID}@vless-ws.example.com:443?type=ws&security=tls&host=cdn.example.com&path=%2Fws&packetEncoding=packet&ed=2048&eh=X-Data#VLESS%20WS%20Early`,

0 commit comments

Comments
 (0)