Sfoglia il codice sorgente

feat(frontend): round-trip XHTTP advanced fields in outbound link parser

Pick up xPaddingBytes, scMaxEachPostBytes, scMinPostsIntervalMs,
uplinkChunkSize, and noGRPCHeader from both vmess:// JSON and the URL
query-param parsers (vless/trojan). The advanced xmux/padding-obfs/
reality-shortId knobs still wait on a follow-up; this slice unblocks
the common case where a phone-issued xhttp link carries non-default
padding or post sizes.
MHSanaei 10 ore fa
parent
commit
2f1a146f45

+ 32 - 13
frontend/src/lib/xray/outbound-link-parser.ts

@@ -7,12 +7,13 @@ import { Base64 } from '@/utils';
 //
 // Scope: address + port + auth + remark, plus the network/security
 // fields the common vmess:// / vless:// links carry as query params.
-// Advanced transport fields (xmux, padding obfs, hysteria udphop,
-// reality short IDs, etc.) are not parsed — the user finishes them
-// in the form after import. This is intentional: a focused parser
-// keeps the surface small; the legacy Outbound.fromLink was ~250
-// lines of dense edge-case handling we don't need to replicate
-// verbatim for the common phone-to-panel workflow.
+// XHTTP advanced fields (xPaddingBytes, scMaxEachPostBytes,
+// scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader) round-trip when
+// present in either the JSON or URL params. xmux, reality shortIds,
+// padding obfs key/header/placement, hysteria udphop are still left
+// to the user to fill in after import — the legacy Outbound.fromLink
+// was ~250 lines of dense edge-case handling we don't need to
+// replicate verbatim for the common phone-to-panel workflow.
 
 type Raw = Record<string, unknown>;
 
@@ -81,11 +82,23 @@ function applyTransportParams(stream: Raw, params: URLSearchParams): void {
       (stream.httpupgradeSettings as Raw).host = host;
       (stream.httpupgradeSettings as Raw).path = path;
       break;
-    case 'xhttp':
-      (stream.xhttpSettings as Raw).host = host;
-      (stream.xhttpSettings as Raw).path = path;
-      if (params.get('mode')) (stream.xhttpSettings as Raw).mode = params.get('mode');
+    case 'xhttp': {
+      const xhttp = stream.xhttpSettings as Raw;
+      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';
       break;
+    }
     case 'tcp':
       // vless/trojan TCP HTTP camouflage rides on header=http+host+path
       if (params.get('headerType') === 'http' || params.get('type') === 'http') {
@@ -157,9 +170,15 @@ export function parseVmessLink(link: string): Raw | null {
       (stream.httpupgradeSettings as Raw).host = json.host ?? '';
       (stream.httpupgradeSettings as Raw).path = json.path ?? '/';
     } else if (network === 'xhttp') {
-      (stream.xhttpSettings as Raw).host = json.host ?? '';
-      (stream.xhttpSettings as Raw).path = json.path ?? '/';
-      if (json.mode) (stream.xhttpSettings as Raw).mode = json.mode;
+      const xhttp = stream.xhttpSettings as Raw;
+      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;
     }
     if (security === 'tls') {
       const tls = stream.tlsSettings as Raw;

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

@@ -49,6 +49,56 @@ describe('parseVmessLink', () => {
   });
 });
 
+describe('parseVmessLink — XHTTP advanced fields', () => {
+  it('round-trips xhttp knobs from the vmess JSON', () => {
+    const json = {
+      v: '2', ps: 'imported-xhttp', add: '1.2.3.4', port: 443,
+      id: '11111111-2222-4333-8444-555555555555', aid: 0, scy: 'auto',
+      net: 'xhttp', host: 'edge.example', path: '/sp', mode: 'stream-up',
+      xPaddingBytes: '500-1500',
+      scMaxEachPostBytes: '2000000',
+      scMinPostsIntervalMs: '60',
+      uplinkChunkSize: 8192,
+      noGRPCHeader: true,
+      tls: 'tls', sni: 'edge.example',
+    };
+    const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
+    const out = parseVmessLink(link);
+    const stream = out?.streamSettings as Record<string, unknown>;
+    const xhttp = stream.xhttpSettings as Record<string, unknown>;
+    expect(xhttp.host).toBe('edge.example');
+    expect(xhttp.path).toBe('/sp');
+    expect(xhttp.mode).toBe('stream-up');
+    expect(xhttp.xPaddingBytes).toBe('500-1500');
+    expect(xhttp.scMaxEachPostBytes).toBe('2000000');
+    expect(xhttp.scMinPostsIntervalMs).toBe('60');
+    expect(xhttp.uplinkChunkSize).toBe(8192);
+    expect(xhttp.noGRPCHeader).toBe(true);
+  });
+});
+
+describe('parseVlessLink — XHTTP advanced fields', () => {
+  it('round-trips xhttp knobs from URL query params', () => {
+    const link
+      = 'vless://[email protected]:443'
+      + '?type=xhttp&security=tls&host=edge.example&path=%2Fsp&mode=stream-up'
+      + '&xPaddingBytes=500-1500&scMaxEachPostBytes=2000000'
+      + '&scMinPostsIntervalMs=60&uplinkChunkSize=8192&noGRPCHeader=true'
+      + '#imported-xhttp';
+    const out = parseVlessLink(link);
+    const stream = out?.streamSettings as Record<string, unknown>;
+    const xhttp = stream.xhttpSettings as Record<string, unknown>;
+    expect(xhttp.host).toBe('edge.example');
+    expect(xhttp.path).toBe('/sp');
+    expect(xhttp.mode).toBe('stream-up');
+    expect(xhttp.xPaddingBytes).toBe('500-1500');
+    expect(xhttp.scMaxEachPostBytes).toBe('2000000');
+    expect(xhttp.scMinPostsIntervalMs).toBe('60');
+    expect(xhttp.uplinkChunkSize).toBe(8192);
+    expect(xhttp.noGRPCHeader).toBe(true);
+  });
+});
+
 describe('parseVlessLink', () => {
   it('parses a vless:// link with reality', () => {
     const link