Forráskód Böngészése

fix(inbounds): apply the legacy xhttp session-key migration when editing

rawInboundToFormValues injected the stored xhttpSettings blob into the form
store without running it through XHttpStreamSettingsSchema, so the
sessionPlacement/sessionKey -> sessionIDPlacement/sessionIDKey rename from
xray-core v26.6.22 (and the v3.4.0 field defaults) never applied on the
edit path. Inbounds saved before the rename opened with blank session
fields, and the stale keys could ride back on save even though the core no
longer reads them. Parse the sub-object through the schema on load, and
lift any stale legacy keys in normalizeXhttpForWire as a backstop.

Closes #5621
MHSanaei 1 napja
szülő
commit
539bcc897c

+ 4 - 2
frontend/src/lib/xray/inbound-form-adapter.ts

@@ -13,7 +13,7 @@ import type { Sniffing } from '@/schemas/primitives';
 import type { z } from 'zod';
 import { normalizeStreamSettingsForWire } from '@/lib/xray/stream-wire-normalize';
 import { canEnableSniffing } from '@/lib/xray/protocol-capabilities';
-import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
+import { XHttpStreamSettingsSchema, XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
 
 const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});
 
@@ -164,7 +164,9 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
     const streamRecord = streamSettings as unknown as Record<string, unknown>;
     const xh = streamRecord.xhttpSettings;
     if (xh && typeof xh === 'object' && !Array.isArray(xh)) {
-      const xhttp = xh as Record<string, unknown>;
+      const parsed = XHttpStreamSettingsSchema.safeParse(xh);
+      const xhttp = (parsed.success ? parsed.data : xh) as Record<string, unknown>;
+      streamRecord.xhttpSettings = xhttp;
       const xmux = xhttp.xmux;
       if (xmux && typeof xmux === 'object' && !Array.isArray(xmux)) {
         xhttp.enableXmux = true;

+ 13 - 0
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -108,6 +108,18 @@ export function validateRealityTarget(target: string): string | undefined {
   return undefined;
 }
 
+function liftLegacyXhttpSessionKeys(obj: Record<string, unknown>): void {
+  const lift = (legacy: string, renamed: string) => {
+    const v = obj[legacy];
+    if ((obj[renamed] === undefined || obj[renamed] === '') && typeof v === 'string' && v !== '') {
+      obj[renamed] = v;
+    }
+    delete obj[legacy];
+  };
+  lift('sessionPlacement', 'sessionIDPlacement');
+  lift('sessionKey', 'sessionIDKey');
+}
+
 function dropEmptyStrings(obj: Record<string, unknown>, keys: readonly string[]): void {
   for (const key of keys) {
     const v = obj[key];
@@ -160,6 +172,7 @@ export function normalizeXhttpForWire(
   side: StreamWireSide,
 ): Record<string, unknown> {
   const out: Record<string, unknown> = { ...raw };
+  liftLegacyXhttpSessionKeys(out);
   const mode = typeof out.mode === 'string' && out.mode !== '' ? out.mode : 'auto';
   const enableXmux = out.enableXmux === true;
   delete out.enableXmux;

+ 49 - 0
frontend/src/test/inbound-form-adapter.test.ts

@@ -7,6 +7,7 @@ import {
   type RawInboundRow,
 } from '@/lib/xray/inbound-form-adapter';
 import { InboundDbFieldsSchema, InboundFormSchema } from '@/schemas/forms/inbound-form';
+import { normalizeXhttpForWire } from '@/lib/xray/stream-wire-normalize';
 import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
 
 // Round-trip: raw DB row → InboundFormValues → wire payload, asserting
@@ -304,3 +305,51 @@ describe('subSortIndex', () => {
     expect(InboundDbFieldsSchema.parse({}).subSortIndex).toBe(1);
   });
 });
+
+describe('legacy xhttp session keys on edit (#5621)', () => {
+  const legacyXhttpRow: RawInboundRow = {
+    ...vlessRow,
+    streamSettings: {
+      network: 'xhttp',
+      security: 'none',
+      xhttpSettings: {
+        path: '/xh',
+        mode: 'packet-up',
+        sessionPlacement: 'cookie',
+        sessionKey: 'x_session',
+      },
+    },
+  };
+
+  it('rawInboundToFormValues lifts sessionPlacement/sessionKey onto the renamed keys', () => {
+    const values = rawInboundToFormValues(legacyXhttpRow);
+    const xhttp = (values.streamSettings as unknown as Record<string, Record<string, unknown>>).xhttpSettings;
+    expect(xhttp.sessionIDPlacement).toBe('cookie');
+    expect(xhttp.sessionIDKey).toBe('x_session');
+    expect(xhttp.sessionPlacement).toBeUndefined();
+    expect(xhttp.sessionKey).toBeUndefined();
+    expect(xhttp.path).toBe('/xh');
+    expect(xhttp.xPaddingBytes).toBe('100-1000');
+  });
+
+  it('formValuesToWirePayload never emits the legacy key names', () => {
+    const values = rawInboundToFormValues(legacyXhttpRow);
+    const payload = formValuesToWirePayload(values);
+    const stream = JSON.parse(payload.streamSettings) as Record<string, Record<string, unknown>>;
+    expect(stream.xhttpSettings.sessionPlacement).toBeUndefined();
+    expect(stream.xhttpSettings.sessionKey).toBeUndefined();
+    expect(stream.xhttpSettings.sessionIDPlacement).toBe('cookie');
+    expect(stream.xhttpSettings.sessionIDKey).toBe('x_session');
+  });
+
+  it('normalizeXhttpForWire lifts stale legacy keys that bypassed the schema', () => {
+    const out = normalizeXhttpForWire(
+      { sessionPlacement: 'header', sessionKey: 'x_raw' },
+      'inbound',
+    );
+    expect(out.sessionIDPlacement).toBe('header');
+    expect(out.sessionIDKey).toBe('x_raw');
+    expect(out.sessionPlacement).toBeUndefined();
+    expect(out.sessionKey).toBeUndefined();
+  });
+});