Skip to content

Commit 7e70ffd

Browse files
committed
feat: 支持 sc-min-posts-interval-ms; 优化 VLESS URI 转换逻辑
1 parent 48cdcad commit 7e70ffd

7 files changed

Lines changed: 1263 additions & 33 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.94",
3+
"version": "2.21.95",
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: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import YAML from '@/utils/yaml';
1818
import _ from 'lodash';
1919

2020
import { Base64 } from 'js-base64';
21+
import { normalizeXhttpScalarUpperBound } from '../xhttp-utils';
2122

2223
function surge_port_hopping(raw) {
2324
const [parts, port_hopping] =
@@ -800,6 +801,22 @@ function URI_VLESS() {
800801
parsedDownloadSettings['x-padding-bytes'] =
801802
downloadSettings.xhttpSettings.xPaddingBytes;
802803
}
804+
const scMaxEachPostBytes =
805+
normalizeXhttpScalarUpperBound(
806+
downloadSettings.xhttpSettings.scMaxEachPostBytes,
807+
);
808+
if (scMaxEachPostBytes != null) {
809+
parsedDownloadSettings['sc-max-each-post-bytes'] =
810+
scMaxEachPostBytes;
811+
}
812+
const scMinPostsIntervalMs =
813+
normalizeXhttpScalarUpperBound(
814+
downloadSettings.xhttpSettings.scMinPostsIntervalMs,
815+
);
816+
if (scMinPostsIntervalMs != null) {
817+
parsedDownloadSettings['sc-min-posts-interval-ms'] =
818+
scMinPostsIntervalMs;
819+
}
803820

804821
const reuseSettings = mapXmuxToReuseSettings(
805822
downloadSettings.xhttpSettings.extra?.xmux,
@@ -1032,31 +1049,19 @@ function URI_VLESS() {
10321049
if (isNotBlank(extra?.['xPaddingBytes'])) {
10331050
xhttpOpts['x-padding-bytes'] = extra['xPaddingBytes'];
10341051
}
1035-
const scMaxEachPostBytes = extra?.['scMaxEachPostBytes'];
1036-
if (
1037-
typeof scMaxEachPostBytes === 'string' ||
1038-
typeof scMaxEachPostBytes === 'number'
1039-
) {
1040-
const normalizedValue = `${scMaxEachPostBytes}`;
1041-
if (/^\s*[1-9]\d*\s*$/.test(normalizedValue)) {
1042-
xhttpOpts['sc-max-each-post-bytes'] = parseInt(
1043-
normalizedValue,
1044-
10,
1045-
);
1046-
} else {
1047-
const rangeMatch = normalizedValue.match(
1048-
/^\s*([1-9]\d*)\s*-\s*([1-9]\d*)\s*$/,
1049-
);
1050-
if (rangeMatch) {
1051-
const lowerBound = parseInt(rangeMatch[1], 10);
1052-
const upperBound = parseInt(rangeMatch[2], 10);
1053-
1054-
if (upperBound >= lowerBound) {
1055-
xhttpOpts['sc-max-each-post-bytes'] =
1056-
upperBound;
1057-
}
1058-
}
1059-
}
1052+
const scMaxEachPostBytes =
1053+
normalizeXhttpScalarUpperBound(extra?.['scMaxEachPostBytes']);
1054+
if (scMaxEachPostBytes != null) {
1055+
xhttpOpts['sc-max-each-post-bytes'] =
1056+
scMaxEachPostBytes;
1057+
}
1058+
const scMinPostsIntervalMs =
1059+
normalizeXhttpScalarUpperBound(
1060+
extra?.['scMinPostsIntervalMs'],
1061+
);
1062+
if (scMinPostsIntervalMs != null) {
1063+
xhttpOpts['sc-min-posts-interval-ms'] =
1064+
scMinPostsIntervalMs;
10601065
}
10611066
if (isPlainObject(extra?.xmux)) {
10621067
const reuseSettings = mapXmuxToReuseSettings(extra.xmux);

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

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Base64 } from 'js-base64';
33
import { isIPv6 } from '@/utils';
44
import { normalizePluginMuxValue } from './utils';
5+
import { normalizeXhttpScalarUpperBound } from '../xhttp-utils';
56

67
function isObject(value) {
78
return value != null && typeof value === 'object' && !Array.isArray(value);
@@ -103,6 +104,22 @@ function buildXhttpDownloadSettings(downloadSettings) {
103104
if (downloadSettings['x-padding-bytes']) {
104105
xhttpSettings.xPaddingBytes = downloadSettings['x-padding-bytes'];
105106
}
107+
if (downloadSettings['sc-max-each-post-bytes'] != null) {
108+
const scMaxEachPostBytes = normalizeXhttpScalarUpperBound(
109+
downloadSettings['sc-max-each-post-bytes'],
110+
);
111+
if (scMaxEachPostBytes != null) {
112+
xhttpSettings.scMaxEachPostBytes = scMaxEachPostBytes;
113+
}
114+
}
115+
if (downloadSettings['sc-min-posts-interval-ms'] != null) {
116+
const scMinPostsIntervalMs = normalizeXhttpScalarUpperBound(
117+
downloadSettings['sc-min-posts-interval-ms'],
118+
);
119+
if (scMinPostsIntervalMs != null) {
120+
xhttpSettings.scMinPostsIntervalMs = scMinPostsIntervalMs;
121+
}
122+
}
106123

107124
const xmux = mapReuseSettingsToXmux(downloadSettings['reuse-settings']);
108125
if (xmux) {
@@ -126,7 +143,20 @@ function buildStructuredVlessExtra(proxy) {
126143
extra.xPaddingBytes = xhttpOpts['x-padding-bytes'];
127144
}
128145
if (xhttpOpts['sc-max-each-post-bytes'] != null) {
129-
extra.scMaxEachPostBytes = xhttpOpts['sc-max-each-post-bytes'];
146+
const scMaxEachPostBytes = normalizeXhttpScalarUpperBound(
147+
xhttpOpts['sc-max-each-post-bytes'],
148+
);
149+
if (scMaxEachPostBytes != null) {
150+
extra.scMaxEachPostBytes = scMaxEachPostBytes;
151+
}
152+
}
153+
if (xhttpOpts['sc-min-posts-interval-ms'] != null) {
154+
const scMinPostsIntervalMs = normalizeXhttpScalarUpperBound(
155+
xhttpOpts['sc-min-posts-interval-ms'],
156+
);
157+
if (scMinPostsIntervalMs != null) {
158+
extra.scMinPostsIntervalMs = scMinPostsIntervalMs;
159+
}
130160
}
131161

132162
const xmux = mapReuseSettingsToXmux(xhttpOpts['reuse-settings']);
@@ -144,13 +174,124 @@ function buildStructuredVlessExtra(proxy) {
144174
return Object.keys(extra).length > 0 ? JSON.stringify(extra) : '';
145175
}
146176

177+
function applyNormalizedXhttpScalar(target, rawKey, value) {
178+
if (!isObject(target)) {
179+
return false;
180+
}
181+
182+
const normalizedValue = normalizeXhttpScalarUpperBound(value);
183+
if (normalizedValue != null) {
184+
if (target[rawKey] === normalizedValue) {
185+
return false;
186+
}
187+
target[rawKey] = normalizedValue;
188+
return true;
189+
}
190+
191+
if (Object.prototype.hasOwnProperty.call(target, rawKey)) {
192+
delete target[rawKey];
193+
return true;
194+
}
195+
196+
return false;
197+
}
198+
199+
function normalizeRawXhttpExtra(proxy) {
200+
if (!proxy._extra) {
201+
return '';
202+
}
203+
204+
let rawExtra;
205+
try {
206+
rawExtra = JSON.parse(proxy._extra);
207+
} catch (e) {
208+
return proxy._extra;
209+
}
210+
211+
if (!isObject(rawExtra) || !isObject(proxy['xhttp-opts'])) {
212+
return proxy._extra;
213+
}
214+
215+
let didChange = false;
216+
const xhttpOpts = proxy['xhttp-opts'];
217+
218+
if (xhttpOpts['sc-max-each-post-bytes'] != null) {
219+
didChange =
220+
applyNormalizedXhttpScalar(
221+
rawExtra,
222+
'scMaxEachPostBytes',
223+
xhttpOpts['sc-max-each-post-bytes'],
224+
) || didChange;
225+
}
226+
if (xhttpOpts['sc-min-posts-interval-ms'] != null) {
227+
didChange =
228+
applyNormalizedXhttpScalar(
229+
rawExtra,
230+
'scMinPostsIntervalMs',
231+
xhttpOpts['sc-min-posts-interval-ms'],
232+
) || didChange;
233+
}
234+
235+
const downloadSettings = xhttpOpts['download-settings'];
236+
if (isObject(downloadSettings)) {
237+
const rawDownloadSettings = isObject(rawExtra.downloadSettings)
238+
? rawExtra.downloadSettings
239+
: undefined;
240+
241+
let rawXhttpSettings = isObject(rawDownloadSettings?.xhttpSettings)
242+
? rawDownloadSettings.xhttpSettings
243+
: undefined;
244+
if (
245+
!rawXhttpSettings &&
246+
(downloadSettings['sc-max-each-post-bytes'] != null ||
247+
downloadSettings['sc-min-posts-interval-ms'] != null)
248+
) {
249+
rawExtra.downloadSettings = isObject(rawExtra.downloadSettings)
250+
? rawExtra.downloadSettings
251+
: {};
252+
rawExtra.downloadSettings.xhttpSettings = {};
253+
rawXhttpSettings = rawExtra.downloadSettings.xhttpSettings;
254+
didChange = true;
255+
}
256+
257+
if (rawXhttpSettings) {
258+
if (downloadSettings['sc-max-each-post-bytes'] != null) {
259+
didChange =
260+
applyNormalizedXhttpScalar(
261+
rawXhttpSettings,
262+
'scMaxEachPostBytes',
263+
downloadSettings['sc-max-each-post-bytes'],
264+
) || didChange;
265+
}
266+
if (downloadSettings['sc-min-posts-interval-ms'] != null) {
267+
didChange =
268+
applyNormalizedXhttpScalar(
269+
rawXhttpSettings,
270+
'scMinPostsIntervalMs',
271+
downloadSettings['sc-min-posts-interval-ms'],
272+
) || didChange;
273+
}
274+
275+
if (Object.keys(rawXhttpSettings).length === 0) {
276+
delete rawExtra.downloadSettings.xhttpSettings;
277+
didChange = true;
278+
if (Object.keys(rawExtra.downloadSettings).length === 0) {
279+
delete rawExtra.downloadSettings;
280+
}
281+
}
282+
}
283+
}
284+
285+
return didChange ? JSON.stringify(rawExtra) : proxy._extra;
286+
}
287+
147288
function buildVlessExtra(proxy) {
148289
if (proxy.network !== 'xhttp') {
149290
return proxy._extra || '';
150291
}
151292

152293
if (proxy._extra) {
153-
return proxy._extra;
294+
return normalizeRawXhttpExtra(proxy);
154295
}
155296

156297
return buildStructuredVlessExtra(proxy);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export function normalizeXhttpScalarUpperBound(value) {
2+
if (typeof value !== 'string' && typeof value !== 'number') {
3+
return undefined;
4+
}
5+
6+
// Mirror Mihomo's ParseRange/GetNormalizedSc* behavior: accept explicit
7+
// positive signs, leading zeros, and ascending ranges, but reject
8+
// zero-valued finals because sc-* values must resolve to > 0.
9+
const parseUnsignedIntegerToken = (token) => {
10+
const normalizedToken = token.trim();
11+
if (!/^\+?\d+$/.test(normalizedToken)) {
12+
return undefined;
13+
}
14+
15+
const parsedInteger = parseInt(normalizedToken, 10);
16+
return Number.isSafeInteger(parsedInteger) ? parsedInteger : undefined;
17+
};
18+
19+
const normalizedValue = `${value}`.trim();
20+
const rangeParts = normalizedValue.split('-');
21+
if (rangeParts.length === 1) {
22+
const normalizedInteger = parseUnsignedIntegerToken(rangeParts[0]);
23+
return normalizedInteger > 0 ? normalizedInteger : undefined;
24+
}
25+
26+
if (rangeParts.length !== 2) {
27+
return undefined;
28+
}
29+
30+
const lowerBound = parseUnsignedIntegerToken(rangeParts[0]);
31+
const upperBound = parseUnsignedIntegerToken(rangeParts[1]);
32+
if (lowerBound == null || upperBound == null) {
33+
return undefined;
34+
}
35+
36+
return upperBound > 0 && upperBound >= lowerBound ? upperBound : undefined;
37+
}

0 commit comments

Comments
 (0)