mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 01:19:34 +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:
@@ -17,6 +17,55 @@ import { Base64 } from '@/utils';
|
||||
|
||||
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 {
|
||||
const stream: Raw = { network, security };
|
||||
switch (network) {
|
||||
@@ -87,16 +136,7 @@ function applyTransportParams(stream: Raw, params: URLSearchParams): void {
|
||||
xhttp.host = host;
|
||||
xhttp.path = path;
|
||||
if (params.get('mode')) xhttp.mode = params.get('mode');
|
||||
const xPad = params.get('xPaddingBytes');
|
||||
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';
|
||||
applyXhttpStringFromParams(xhttp, params);
|
||||
break;
|
||||
}
|
||||
case 'tcp':
|
||||
@@ -174,11 +214,7 @@ export function parseVmessLink(link: string): Raw | null {
|
||||
xhttp.host = json.host ?? '';
|
||||
xhttp.path = json.path ?? '/';
|
||||
if (json.mode) xhttp.mode = json.mode;
|
||||
if (typeof json.xPaddingBytes === 'string') xhttp.xPaddingBytes = json.xPaddingBytes;
|
||||
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;
|
||||
applyXhttpStringFromJson(xhttp, json);
|
||||
}
|
||||
if (security === 'tls') {
|
||||
const tls = stream.tlsSettings as Raw;
|
||||
|
||||
@@ -75,6 +75,36 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
|
||||
expect(xhttp.uplinkChunkSize).toBe(8192);
|
||||
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', () => {
|
||||
@@ -97,6 +127,28 @@ describe('parseVlessLink — XHTTP advanced fields', () => {
|
||||
expect(xhttp.uplinkChunkSize).toBe(8192);
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user