Skip to content

Commit b6969e7

Browse files
committed
feat: 兼容 Surge headers/ws-headers 的嵌套引号与逗号值,优化解析和输出
- Surge parser 重构 headers/ws-headers 分隔与引号边界识别,新增可配置分隔符并修复带嵌套引号/逗号值时的误切分问题(含 vmess ws-headers 的 pipe 场景) - Surge producer 调整 headers 与 ws-headers 的序列化规则,统一用原值加引号输出并避免额外转义导致的值变化 - 补充/更新 parser 与 producer 测试用例,覆盖嵌套引号、逗号值与 ws-headers round-trip
1 parent 10a80e3 commit b6969e7

5 files changed

Lines changed: 181 additions & 96 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.23.23",
3+
"version": "2.23.25",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/core/proxy-utils/parsers/peggy/surge.js

Lines changed: 89 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,10 @@ const grammars = String.raw`
4242
(quote === '"' || quote === "'") &&
4343
trimmed[trimmed.length - 1] === quote
4444
) {
45-
return trimmed.slice(1, -1).replace(/\\(["'\\])/g, "$1");
46-
}
47-
48-
return trimmed.replace(/\\(["'\\])/g, "$1");
49-
}
50-
function isEscaped(text, index) {
51-
let count = 0;
52-
let cursor = index - 1;
53-
54-
while (cursor >= 0 && text[cursor] === "\\") {
55-
count++;
56-
cursor--;
45+
return trimmed.slice(1, -1);
5746
}
5847
59-
return count % 2 === 1;
48+
return trimmed;
6049
}
6150
function readQuotedHeaderKey(text, start) {
6251
const quote = text[start];
@@ -65,11 +54,6 @@ const grammars = String.raw`
6554
6655
while (index < text.length) {
6756
const char = text[index];
68-
if (char === "\\" && index + 1 < text.length) {
69-
hasKey = true;
70-
index += 2;
71-
continue;
72-
}
7357
if (char === quote) {
7458
return hasKey ? index + 1 : -1;
7559
}
@@ -125,15 +109,35 @@ const grammars = String.raw`
125109
while (index < text.length && /\s/.test(text[index])) index++;
126110
return text[index] === ":";
127111
}
128-
function isHeaderValueQuoteEnd(text, index) {
112+
function isOptionStart(text, start) {
113+
let index = start;
114+
while (index < text.length && /\s/.test(text[index])) index++;
115+
116+
const keyStart = index;
117+
while (index < text.length && /[0-9A-Za-z-]/.test(text[index])) index++;
118+
if (index === keyStart) return false;
119+
120+
while (index < text.length && /\s/.test(text[index])) index++;
121+
return text[index] === "=";
122+
}
123+
function isHeaderValueQuoteEnd(text, index, pairSeparator, allowCommaEnd, containerQuote) {
129124
let cursor = index + 1;
130125
while (cursor < text.length && /\s/.test(text[cursor])) cursor++;
131126
132-
return (
133-
cursor >= text.length ||
134-
text[cursor] === "," ||
135-
(text[cursor] === ";" && isHeaderKeyStart(text, cursor + 1))
136-
);
127+
if (cursor >= text.length) return true;
128+
if (allowCommaEnd && text[cursor] === "," && isOptionStart(text, cursor + 1)) {
129+
return true;
130+
}
131+
if (text[cursor] === pairSeparator && isHeaderKeyStart(text, cursor + 1)) {
132+
return true;
133+
}
134+
if (containerQuote && text[cursor] === containerQuote) {
135+
let next = cursor + 1;
136+
while (next < text.length && /\s/.test(text[next])) next++;
137+
return next >= text.length || text[next] === ",";
138+
}
139+
140+
return false;
137141
}
138142
function findHeaderSeparator(pair) {
139143
let quote = "";
@@ -142,10 +146,6 @@ const grammars = String.raw`
142146
const char = pair[index];
143147
144148
if (quote) {
145-
if (char === "\\" && index + 1 < pair.length) {
146-
index++;
147-
continue;
148-
}
149149
if (char === quote) {
150150
quote = "";
151151
}
@@ -164,7 +164,7 @@ const grammars = String.raw`
164164
165165
return -1;
166166
}
167-
function readUnquotedHeadersEnd(text, start) {
167+
function readUnquotedHeadersEnd(text, start, pairSeparator) {
168168
let index = start;
169169
let quote = "";
170170
let quoteRole = "";
@@ -174,14 +174,10 @@ const grammars = String.raw`
174174
const char = text[index];
175175
176176
if (quote) {
177-
if (char === "\\" && index + 1 < text.length) {
178-
index += 2;
179-
continue;
180-
}
181177
if (char === quote) {
182178
if (
183179
quoteRole === "key" ||
184-
isHeaderValueQuoteEnd(text, index)
180+
isHeaderValueQuoteEnd(text, index, pairSeparator, true)
185181
) {
186182
quote = "";
187183
quoteRole = "";
@@ -204,7 +200,7 @@ const grammars = String.raw`
204200
continue;
205201
}
206202
207-
if (char === ";" && isHeaderKeyStart(text, index + 1)) {
203+
if (char === pairSeparator && isHeaderKeyStart(text, index + 1)) {
208204
seenSeparator = false;
209205
index++;
210206
continue;
@@ -216,37 +212,75 @@ const grammars = String.raw`
216212
217213
return index;
218214
}
219-
function readQuotedHeadersEnd(text, start) {
215+
function readQuotedHeadersEnd(text, start, pairSeparator) {
220216
const quote = text[start];
221217
let index = start + 1;
218+
let innerQuote = "";
219+
let quoteRole = "";
220+
let seenSeparator = false;
222221
223222
while (index < text.length) {
224-
if (text[index] === quote && !isEscaped(text, index)) {
223+
const char = text[index];
224+
225+
if (innerQuote) {
226+
if (char === innerQuote) {
227+
if (
228+
quoteRole === "key" ||
229+
isHeaderValueQuoteEnd(text, index, pairSeparator, false, quote)
230+
) {
231+
innerQuote = "";
232+
quoteRole = "";
233+
}
234+
}
235+
index++;
236+
continue;
237+
}
238+
239+
if (char === quote) {
225240
let cursor = index + 1;
226241
while (cursor < text.length && /\s/.test(text[cursor])) cursor++;
227242
if (cursor >= text.length || text[cursor] === ",") {
228243
return index + 1;
229244
}
230245
}
246+
247+
if (char === '"' || char === "'") {
248+
innerQuote = char;
249+
quoteRole = seenSeparator ? "value" : "key";
250+
index++;
251+
continue;
252+
}
253+
254+
if (char === ":" && !seenSeparator) {
255+
seenSeparator = true;
256+
index++;
257+
continue;
258+
}
259+
260+
if (char === pairSeparator && isHeaderKeyStart(text, index + 1)) {
261+
seenSeparator = false;
262+
index++;
263+
continue;
264+
}
231265
index++;
232266
}
233267
234268
return text.length;
235269
}
236-
function readHeadersEnd(text, start) {
270+
function readHeadersEnd(text, start, pairSeparator) {
237271
let index = start;
238272
while (index < text.length && /\s/.test(text[index])) index++;
239273
240274
if (
241275
(text[index] === '"' || text[index] === "'") &&
242276
!startsWithQuotedHeaderKey(text.slice(index))
243277
) {
244-
return readQuotedHeadersEnd(text, index);
278+
return readQuotedHeadersEnd(text, index, pairSeparator);
245279
}
246280
247-
return readUnquotedHeadersEnd(text, start);
281+
return readUnquotedHeadersEnd(text, start, pairSeparator);
248282
}
249-
function splitHeaders(headers) {
283+
function splitHeaders(headers, pairSeparator) {
250284
const result = [];
251285
let start = 0;
252286
let quote = "";
@@ -257,14 +291,10 @@ const grammars = String.raw`
257291
const char = headers[index];
258292
259293
if (quote) {
260-
if (char === "\\" && index + 1 < headers.length) {
261-
index++;
262-
continue;
263-
}
264294
if (char === quote) {
265295
if (
266296
quoteRole === "key" ||
267-
isHeaderValueQuoteEnd(headers, index)
297+
isHeaderValueQuoteEnd(headers, index, pairSeparator, false)
268298
) {
269299
quote = "";
270300
quoteRole = "";
@@ -284,7 +314,7 @@ const grammars = String.raw`
284314
continue;
285315
}
286316
287-
if (char === ";" && isHeaderKeyStart(headers, index + 1)) {
317+
if (char === pairSeparator && isHeaderKeyStart(headers, index + 1)) {
288318
result.push(headers.slice(start, index));
289319
start = index + 1;
290320
seenSeparator = false;
@@ -294,9 +324,9 @@ const grammars = String.raw`
294324
result.push(headers.slice(start));
295325
return result;
296326
}
297-
function parseHeaders(headers) {
327+
function parseHeaders(headers, pairSeparator) {
298328
const result = {};
299-
splitHeaders(stripOuterHeadersQuotes(headers)).forEach((pair) => {
329+
splitHeaders(stripOuterHeadersQuotes(headers), pairSeparator).forEach((pair) => {
300330
const index = findHeaderSeparator(pair);
301331
if (index === -1) return;
302332
@@ -496,24 +526,23 @@ method = comma "encrypt-method" equals cipher:cipher {
496526
cipher = ("aes-128-cfb"/"aes-128-ctr"/"aes-128-gcm"/"aes-192-cfb"/"aes-192-ctr"/"aes-192-gcm"/"aes-256-cfb"/"aes-256-ctr"/"aes-256-gcm"/"bf-cfb"/"camellia-128-cfb"/"camellia-192-cfb"/"camellia-256-cfb"/"cast5-cfb"/"chacha20-ietf-poly1305"/"chacha20-ietf"/"chacha20-poly1305"/"chacha20"/"des-cfb"/"idea-cfb"/"none"/"rc2-cfb"/"rc4-md5"/"rc4"/"salsa20"/"seed-cfb"/"xchacha20-ietf-poly1305"/"2022-blake3-aes-128-gcm"/"2022-blake3-aes-256-gcm");
497527
498528
ws = comma "ws" equals flag:bool { obfs.type = "ws"; }
499-
ws_headers = comma "ws-headers" equals headers:$[^,]+ {
500-
const pairs = headers.split("|");
501-
const result = {};
502-
pairs.forEach(pair => {
503-
const [key, value] = pair.trim().split(":");
504-
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
505-
})
506-
obfs["ws-headers"] = result;
507-
}
529+
ws_headers = comma "ws-headers" equals & {
530+
const start = peg$currPos;
531+
const index = readHeadersEnd(input, start, "|");
532+
533+
$.headers = input.substring(start, index);
534+
peg$currPos = index;
535+
return $.headers.trim().length > 0;
536+
} { obfs["ws-headers"] = parseHeaders($.headers, "|"); }
508537
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
509538
headers = comma "headers" equals & {
510539
const start = peg$currPos;
511-
const index = readHeadersEnd(input, start);
540+
const index = readHeadersEnd(input, start, ";");
512541
513542
$.headers = input.substring(start, index);
514543
peg$currPos = index;
515544
return $.headers.trim().length > 0;
516-
} { proxy.headers = parseHeaders($.headers); }
545+
} { proxy.headers = parseHeaders($.headers, ";"); }
517546
518547
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
519548
obfs_host = comma "obfs-host" equals match:[^,]+ { obfs.host = match.join("").replace(/^"(.*)"$/, '$1'); };

backend/src/core/proxy-utils/producers/surge.js

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,15 @@ function stripSurgeQuotes(value) {
3232
(quote === '"' || quote === "'") &&
3333
trimmed[trimmed.length - 1] === quote
3434
) {
35-
return trimmed.slice(1, -1).replace(/\\(["'\\])/g, '$1');
35+
return trimmed.slice(1, -1);
3636
}
3737

38-
return trimmed.replace(/\\(["'\\])/g, '$1');
38+
return trimmed;
3939
}
4040

4141
function quoteSurgeValue(value) {
42-
return `"${String(stripSurgeQuotes(value))
43-
.replace(/\\/g, '\\\\')
44-
.replace(/"/g, '\\"')}"`;
42+
const text = String(stripSurgeQuotes(value));
43+
return `"${text}"`;
4544
}
4645

4746
function hasNonBlankValue(value) {
@@ -961,23 +960,23 @@ function socks5(proxy) {
961960
function appendHeaders(result, proxy) {
962961
const value = formatHeaders(proxy.headers);
963962
if (isNotBlank(value)) {
964-
result.append(`,headers="${value}"`);
963+
result.append(`,headers=${quoteSurgeValue(value)}`);
965964
}
966965
}
967966

968967
function formatHeaders(headers) {
968+
return formatHeaderMap(headers, ';');
969+
}
970+
971+
function formatHeaderMap(headers, separator) {
969972
if (!headers || typeof headers !== 'object') {
970973
return '';
971974
}
972975

973976
return Object.entries(headers)
974977
.filter(([key, value]) => isNotBlank(key) && value != null)
975-
.map(([key, value]) => `${key}:"${escapeHeaderValue(value)}"`)
976-
.join(';');
977-
}
978-
979-
function escapeHeaderValue(value) {
980-
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
978+
.map(([key, value]) => `${key}:${quoteSurgeValue(value)}`)
979+
.join(separator);
981980
}
982981

983982
function snell(proxy) {
@@ -1458,17 +1457,9 @@ function handleTransport(result, proxy, includeUnsupportedProxy) {
14581457
);
14591458
if (isPresent(proxy, 'ws-opts.headers')) {
14601459
const headers = proxy['ws-opts'].headers;
1461-
const value = Object.keys(headers)
1462-
.map((k) => {
1463-
let v = headers[k];
1464-
// if (['Host'].includes(k)) {
1465-
v = `"${v}"`;
1466-
// }
1467-
return `${k}:${v}`;
1468-
})
1469-
.join('|');
1460+
const value = formatHeaderMap(headers, '|');
14701461
if (isNotBlank(value)) {
1471-
result.append(`,ws-headers=${value}`);
1462+
result.append(`,ws-headers=${quoteSurgeValue(value)}`);
14721463
}
14731464
}
14741465
}

0 commit comments

Comments
 (0)