mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(frontend): round-trip XHTTP padding-obfs + remaining advanced knobs
Extract the XHTTP key-mapping into typed string/number/bool key arrays
applied by both the URL query-param branch and the vmess JSON branch.
The parser now covers xPaddingObfsMode + xPaddingKey/Header/Placement/
Method, sessionKey/seqKey/uplinkData{Placement,Key}, noSSEHeader,
scMaxBufferedPosts, scStreamUpServerSecs, serverMaxHeaderBytes, and
uplinkHTTPMethod alongside the previous five XHTTP fields. Two new
round-trip tests cover the padding-obfs surface on both link forms.
This commit is contained in:
parent
2f1a146f45
commit
34590dc327
2 changed files with 103 additions and 15 deletions
|
|
@ -17,6 +17,55 @@ import { Base64 } from '@/utils';
|
||||||
|
|
||||||
type Raw = Record<string, unknown>;
|
type Raw = Record<string, unknown>;
|
||||||
|
|
||||||
|
// XHTTP knob keys grouped by wire type. Used by both the URL query-param
|
||||||
|
// (vless/trojan) branch and the vmess JSON branch to consistently pull
|
||||||
|
// the same set of advanced fields when present. Keep order ~stable to
|
||||||
|
// match the schema's authoring order so diffs read naturally.
|
||||||
|
const XHTTP_STRING_KEYS = [
|
||||||
|
'xPaddingBytes', 'xPaddingKey', 'xPaddingHeader', 'xPaddingPlacement',
|
||||||
|
'xPaddingMethod', 'sessionPlacement', 'sessionKey', 'seqPlacement',
|
||||||
|
'seqKey', 'uplinkDataPlacement', 'uplinkDataKey', 'scMaxEachPostBytes',
|
||||||
|
'scMinPostsIntervalMs', 'scStreamUpServerSecs', 'uplinkHTTPMethod',
|
||||||
|
] as const;
|
||||||
|
const XHTTP_NUMBER_KEYS = [
|
||||||
|
'scMaxBufferedPosts', 'serverMaxHeaderBytes', 'uplinkChunkSize',
|
||||||
|
] as const;
|
||||||
|
const XHTTP_BOOL_KEYS = [
|
||||||
|
'xPaddingObfsMode', 'noSSEHeader', 'noGRPCHeader',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function asBool(s: string | null): boolean | undefined {
|
||||||
|
if (s === null) return undefined;
|
||||||
|
return s === 'true' || s === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyXhttpStringFromParams(xhttp: Raw, params: URLSearchParams): void {
|
||||||
|
for (const k of XHTTP_STRING_KEYS) {
|
||||||
|
const v = params.get(k);
|
||||||
|
if (v !== null && v !== '') xhttp[k] = v;
|
||||||
|
}
|
||||||
|
for (const k of XHTTP_NUMBER_KEYS) {
|
||||||
|
const v = params.get(k);
|
||||||
|
if (v !== null && v !== '') xhttp[k] = Number(v) || 0;
|
||||||
|
}
|
||||||
|
for (const k of XHTTP_BOOL_KEYS) {
|
||||||
|
const v = params.get(k);
|
||||||
|
if (v !== null && v !== '') xhttp[k] = asBool(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyXhttpStringFromJson(xhttp: Raw, json: Record<string, unknown>): void {
|
||||||
|
for (const k of XHTTP_STRING_KEYS) {
|
||||||
|
if (typeof json[k] === 'string') xhttp[k] = json[k];
|
||||||
|
}
|
||||||
|
for (const k of XHTTP_NUMBER_KEYS) {
|
||||||
|
if (typeof json[k] === 'number') xhttp[k] = json[k];
|
||||||
|
}
|
||||||
|
for (const k of XHTTP_BOOL_KEYS) {
|
||||||
|
if (typeof json[k] === 'boolean') xhttp[k] = json[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildStream(network: string, security: string): Raw {
|
function buildStream(network: string, security: string): Raw {
|
||||||
const stream: Raw = { network, security };
|
const stream: Raw = { network, security };
|
||||||
switch (network) {
|
switch (network) {
|
||||||
|
|
@ -87,16 +136,7 @@ function applyTransportParams(stream: Raw, params: URLSearchParams): void {
|
||||||
xhttp.host = host;
|
xhttp.host = host;
|
||||||
xhttp.path = path;
|
xhttp.path = path;
|
||||||
if (params.get('mode')) xhttp.mode = params.get('mode');
|
if (params.get('mode')) xhttp.mode = params.get('mode');
|
||||||
const xPad = params.get('xPaddingBytes');
|
applyXhttpStringFromParams(xhttp, params);
|
||||||
if (xPad) xhttp.xPaddingBytes = xPad;
|
|
||||||
const scMax = params.get('scMaxEachPostBytes');
|
|
||||||
if (scMax) xhttp.scMaxEachPostBytes = scMax;
|
|
||||||
const scMin = params.get('scMinPostsIntervalMs');
|
|
||||||
if (scMin) xhttp.scMinPostsIntervalMs = scMin;
|
|
||||||
const upChunk = params.get('uplinkChunkSize');
|
|
||||||
if (upChunk) xhttp.uplinkChunkSize = Number(upChunk) || 0;
|
|
||||||
const noGrpc = params.get('noGRPCHeader');
|
|
||||||
if (noGrpc) xhttp.noGRPCHeader = noGrpc === 'true' || noGrpc === '1';
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
|
|
@ -174,11 +214,7 @@ export function parseVmessLink(link: string): Raw | null {
|
||||||
xhttp.host = json.host ?? '';
|
xhttp.host = json.host ?? '';
|
||||||
xhttp.path = json.path ?? '/';
|
xhttp.path = json.path ?? '/';
|
||||||
if (json.mode) xhttp.mode = json.mode;
|
if (json.mode) xhttp.mode = json.mode;
|
||||||
if (typeof json.xPaddingBytes === 'string') xhttp.xPaddingBytes = json.xPaddingBytes;
|
applyXhttpStringFromJson(xhttp, json);
|
||||||
if (typeof json.scMaxEachPostBytes === 'string') xhttp.scMaxEachPostBytes = json.scMaxEachPostBytes;
|
|
||||||
if (typeof json.scMinPostsIntervalMs === 'string') xhttp.scMinPostsIntervalMs = json.scMinPostsIntervalMs;
|
|
||||||
if (typeof json.uplinkChunkSize === 'number') xhttp.uplinkChunkSize = json.uplinkChunkSize;
|
|
||||||
if (typeof json.noGRPCHeader === 'boolean') xhttp.noGRPCHeader = json.noGRPCHeader;
|
|
||||||
}
|
}
|
||||||
if (security === 'tls') {
|
if (security === 'tls') {
|
||||||
const tls = stream.tlsSettings as Raw;
|
const tls = stream.tlsSettings as Raw;
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,36 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
|
||||||
expect(xhttp.uplinkChunkSize).toBe(8192);
|
expect(xhttp.uplinkChunkSize).toBe(8192);
|
||||||
expect(xhttp.noGRPCHeader).toBe(true);
|
expect(xhttp.noGRPCHeader).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('round-trips xhttp padding-obfs knobs from the vmess JSON', () => {
|
||||||
|
const json = {
|
||||||
|
v: '2', ps: 'imported-pad', add: '1.2.3.4', port: 443,
|
||||||
|
id: '11111111-2222-4333-8444-555555555555', aid: 0, scy: 'auto',
|
||||||
|
net: 'xhttp', host: 'edge.example', path: '/sp',
|
||||||
|
xPaddingObfsMode: true,
|
||||||
|
xPaddingKey: 'secret-key',
|
||||||
|
xPaddingHeader: 'X-Pad',
|
||||||
|
xPaddingPlacement: 'header',
|
||||||
|
xPaddingMethod: 'random',
|
||||||
|
sessionKey: 'X-Session',
|
||||||
|
seqKey: 'X-Seq',
|
||||||
|
noSSEHeader: true,
|
||||||
|
scMaxBufferedPosts: 50,
|
||||||
|
tls: 'tls',
|
||||||
|
};
|
||||||
|
const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
|
||||||
|
const out = parseVmessLink(link);
|
||||||
|
const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
|
||||||
|
expect(xhttp.xPaddingObfsMode).toBe(true);
|
||||||
|
expect(xhttp.xPaddingKey).toBe('secret-key');
|
||||||
|
expect(xhttp.xPaddingHeader).toBe('X-Pad');
|
||||||
|
expect(xhttp.xPaddingPlacement).toBe('header');
|
||||||
|
expect(xhttp.xPaddingMethod).toBe('random');
|
||||||
|
expect(xhttp.sessionKey).toBe('X-Session');
|
||||||
|
expect(xhttp.seqKey).toBe('X-Seq');
|
||||||
|
expect(xhttp.noSSEHeader).toBe(true);
|
||||||
|
expect(xhttp.scMaxBufferedPosts).toBe(50);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseVlessLink — XHTTP advanced fields', () => {
|
describe('parseVlessLink — XHTTP advanced fields', () => {
|
||||||
|
|
@ -97,6 +127,28 @@ describe('parseVlessLink — XHTTP advanced fields', () => {
|
||||||
expect(xhttp.uplinkChunkSize).toBe(8192);
|
expect(xhttp.uplinkChunkSize).toBe(8192);
|
||||||
expect(xhttp.noGRPCHeader).toBe(true);
|
expect(xhttp.noGRPCHeader).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('round-trips xhttp padding-obfs knobs from URL query params', () => {
|
||||||
|
const link
|
||||||
|
= 'vless://uuid@srv.example:443'
|
||||||
|
+ '?type=xhttp&security=tls&host=edge.example&path=%2Fsp'
|
||||||
|
+ '&xPaddingObfsMode=true&xPaddingKey=secret-key&xPaddingHeader=X-Pad'
|
||||||
|
+ '&xPaddingPlacement=header&xPaddingMethod=random'
|
||||||
|
+ '&sessionKey=X-Session&seqKey=X-Seq&noSSEHeader=true'
|
||||||
|
+ '&scMaxBufferedPosts=50'
|
||||||
|
+ '#imported-pad';
|
||||||
|
const out = parseVlessLink(link);
|
||||||
|
const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
|
||||||
|
expect(xhttp.xPaddingObfsMode).toBe(true);
|
||||||
|
expect(xhttp.xPaddingKey).toBe('secret-key');
|
||||||
|
expect(xhttp.xPaddingHeader).toBe('X-Pad');
|
||||||
|
expect(xhttp.xPaddingPlacement).toBe('header');
|
||||||
|
expect(xhttp.xPaddingMethod).toBe('random');
|
||||||
|
expect(xhttp.sessionKey).toBe('X-Session');
|
||||||
|
expect(xhttp.seqKey).toBe('X-Seq');
|
||||||
|
expect(xhttp.noSSEHeader).toBe(true);
|
||||||
|
expect(xhttp.scMaxBufferedPosts).toBe(50);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseVlessLink', () => {
|
describe('parseVlessLink', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue