Ver código fonte

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.
MHSanaei 8 horas atrás
pai
commit
34590dc327

+ 51 - 15
frontend/src/lib/xray/outbound-link-parser.ts

@@ -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;

+ 52 - 0
frontend/src/test/outbound-link-parser.test.ts

@@ -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://[email protected]: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', () => {