Skip to content

Commit 2ffe572

Browse files
committed
fix: 修复 sing-box ech
1 parent 43c5fc2 commit 2ffe572

4 files changed

Lines changed: 234 additions & 2 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.74",
3+
"version": "2.21.75",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

backend/src/core/proxy-utils/producers/sing-box.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,34 @@ const grpcParser = (proxy, parsedProxy) => {
244244
parsedProxy.transport = transport;
245245
};
246246

247+
const normalizePemLines = (value, label) => {
248+
const items = Array.isArray(value) ? value : [value];
249+
const lines = [];
250+
251+
for (const item of items) {
252+
const normalized = `${item}`
253+
.trim()
254+
.replace(/\\r\\n/g, '\n')
255+
.replace(/\\n/g, '\n');
256+
if (normalized === '') continue;
257+
258+
for (const line of normalized.split(/\r?\n/)) {
259+
const trimmed = line.trim();
260+
if (trimmed !== '') lines.push(trimmed);
261+
}
262+
}
263+
264+
if (lines.length === 0) return undefined;
265+
if (lines.some((line) => /^-----BEGIN [A-Za-z0-9 -]+-----$/.test(line))) {
266+
return lines;
267+
}
268+
return [
269+
`-----BEGIN ${label}-----`,
270+
...lines,
271+
`-----END ${label}-----`,
272+
];
273+
};
274+
247275
const tlsParser = (proxy, parsedProxy) => {
248276
if (proxy.tls) parsedProxy.tls.enabled = true;
249277
if (proxy.servername && proxy.servername !== '')
@@ -284,7 +312,11 @@ const tlsParser = (proxy, parsedProxy) => {
284312
} else if (proxy['ech-opts'] && isPlainObject(proxy['ech-opts'])) {
285313
parsedProxy.tls.ech = parsedProxy.tls.ech || {};
286314
parsedProxy.tls.ech.enabled = proxy['ech-opts'].enable;
287-
parsedProxy.tls.ech.config = proxy['ech-opts'].config;
315+
const echOptsConfig = proxy['ech-opts'].config;
316+
if (Array.isArray(echOptsConfig) || typeof echOptsConfig === 'string') {
317+
const config = normalizePemLines(echOptsConfig, 'ECH CONFIGS');
318+
if (config) parsedProxy.tls.ech.config = config;
319+
}
288320
parsedProxy.tls.ech.query_server_name =
289321
proxy['ech-opts']['query-server-name'];
290322
parsedProxy.tls.ech.config_path = proxy['ech-opts']['config-path'];

backend/src/test/proxy-producers/structured.spec.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,39 @@ describe('Proxy structured producers', function () {
713713
});
714714
});
715715

716+
it('normalizes sing-box ech PEM config strings with escaped newlines', function () {
717+
const output = loadProducedJson('sing-box', {
718+
type: 'vless',
719+
name: 'ECH PEM',
720+
server: 'ech.example.com',
721+
port: 443,
722+
uuid: UUID,
723+
tls: true,
724+
'ech-opts': {
725+
enable: true,
726+
config: [
727+
'-----BEGIN ECH CONFIGS-----\\nZWNoLWNvbmZpZw==\\n-----END ECH CONFIGS-----',
728+
],
729+
},
730+
});
731+
732+
expectSubset(output.outbounds[0], {
733+
tag: 'ECH PEM',
734+
tls: {
735+
enabled: true,
736+
server_name: 'ech.example.com',
737+
ech: {
738+
enabled: true,
739+
config: [
740+
'-----BEGIN ECH CONFIGS-----',
741+
'ZWNoLWNvbmZpZw==',
742+
'-----END ECH CONFIGS-----',
743+
],
744+
},
745+
},
746+
});
747+
});
748+
716749
it('omits xhttp proxies from sing-box exports', function () {
717750
const output = loadProducedJson('sing-box', {
718751
type: 'vless',
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { expect } from 'chai';
2+
import { after, before, beforeEach, describe, it } from 'mocha';
3+
import fs from 'fs';
4+
import os from 'os';
5+
import path from 'path';
6+
7+
import {
8+
HEADERS_RESOURCE_CACHE_KEY,
9+
RESOURCE_CACHE_KEY,
10+
SETTINGS_KEY,
11+
} from '@/constants';
12+
13+
let $;
14+
let openApi;
15+
let download;
16+
let resourceCache;
17+
let headersResourceCache;
18+
let originalRead;
19+
let originalWrite;
20+
let originalInfo;
21+
let originalError;
22+
let originalHTTP;
23+
let originalENV;
24+
let state;
25+
let tempDir;
26+
let previousDataBasePath;
27+
let capturedUrls;
28+
let errorLogs;
29+
30+
describe('download github proxy regex', function () {
31+
before(function () {
32+
previousDataBasePath = process.env.SUB_STORE_DATA_BASE_PATH;
33+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sub-store-download-'));
34+
process.env.SUB_STORE_DATA_BASE_PATH = tempDir;
35+
36+
({ default: $ } = require('@/core/app'));
37+
openApi = require('@/vendor/open-api');
38+
({ default: resourceCache } = require('@/utils/resource-cache'));
39+
({ default: headersResourceCache } = require(
40+
'@/utils/headers-resource-cache'
41+
));
42+
({ default: download } = require('@/utils/download'));
43+
44+
originalRead = $.read.bind($);
45+
originalWrite = $.write.bind($);
46+
originalInfo = $.info.bind($);
47+
originalError = $.error.bind($);
48+
originalHTTP = openApi.HTTP;
49+
originalENV = openApi.ENV;
50+
});
51+
52+
after(function () {
53+
if ($) {
54+
$.read = originalRead;
55+
$.write = originalWrite;
56+
$.info = originalInfo;
57+
$.error = originalError;
58+
}
59+
60+
if (openApi) {
61+
openApi.HTTP = originalHTTP;
62+
openApi.ENV = originalENV;
63+
}
64+
65+
if (previousDataBasePath == null) {
66+
delete process.env.SUB_STORE_DATA_BASE_PATH;
67+
} else {
68+
process.env.SUB_STORE_DATA_BASE_PATH = previousDataBasePath;
69+
}
70+
71+
if (tempDir) {
72+
fs.rmSync(tempDir, { recursive: true, force: true });
73+
}
74+
});
75+
76+
beforeEach(function () {
77+
capturedUrls = [];
78+
errorLogs = [];
79+
state = {
80+
[SETTINGS_KEY]: {
81+
githubProxy: 'https://ghproxy.test',
82+
githubProxyRegex: 'raw\\.githubusercontent\\.com',
83+
},
84+
[RESOURCE_CACHE_KEY]: '{}',
85+
[HEADERS_RESOURCE_CACHE_KEY]: '{}',
86+
};
87+
88+
$.read = (key) => state[key];
89+
$.write = (data, key) => {
90+
state[key] = data;
91+
return true;
92+
};
93+
$.info = () => {};
94+
$.error = (message) => {
95+
errorLogs.push(message);
96+
};
97+
98+
openApi.ENV = () => ({
99+
isNode: true,
100+
isStash: false,
101+
isLoon: false,
102+
isShadowRocket: false,
103+
isQX: false,
104+
isSurge: false,
105+
isGUIforCores: false,
106+
isEgern: false,
107+
isLanceX: false,
108+
});
109+
openApi.HTTP = () => ({
110+
get: async ({ url }) => {
111+
capturedUrls.push(url);
112+
return {
113+
body: 'test-body',
114+
headers: {},
115+
statusCode: 200,
116+
};
117+
},
118+
});
119+
120+
resourceCache.revokeAll();
121+
headersResourceCache.revokeAll();
122+
});
123+
124+
it('prefixes matching download urls with the github proxy', async function () {
125+
await download(
126+
'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
127+
);
128+
129+
expect(capturedUrls).to.deep.equal([
130+
'https://ghproxy.test/https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
131+
]);
132+
});
133+
134+
it('keeps download urls unchanged when the regex does not match', async function () {
135+
await download('https://example.com/archive.txt');
136+
137+
expect(capturedUrls).to.deep.equal([
138+
'https://example.com/archive.txt',
139+
]);
140+
});
141+
142+
it('matches regex patterns case-insensitively by default', async function () {
143+
state[SETTINGS_KEY].githubProxyRegex = '^https://RAW\\.GITHUBUSERCONTENT\\.COM';
144+
145+
await download(
146+
'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
147+
);
148+
149+
expect(capturedUrls).to.deep.equal([
150+
'https://ghproxy.test/https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
151+
]);
152+
});
153+
154+
it('skips proxy prefixing when the regex is invalid', async function () {
155+
state[SETTINGS_KEY].githubProxyRegex = '[';
156+
157+
await download(
158+
'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
159+
);
160+
161+
expect(capturedUrls).to.deep.equal([
162+
'https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/README.md',
163+
]);
164+
expect(errorLogs).to.have.length(1);
165+
expect(errorLogs[0]).to.contain('GitHub 加速代理匹配正则无效');
166+
});
167+
});

0 commit comments

Comments
 (0)