diff --git a/frontend/src/lib/xray/inbound-form-adapter.ts b/frontend/src/lib/xray/inbound-form-adapter.ts index 86151c5ff..dc053b828 100644 --- a/frontend/src/lib/xray/inbound-form-adapter.ts +++ b/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; const xh = streamRecord.xhttpSettings; if (xh && typeof xh === 'object' && !Array.isArray(xh)) { - const xhttp = xh as Record; + const parsed = XHttpStreamSettingsSchema.safeParse(xh); + const xhttp = (parsed.success ? parsed.data : xh) as Record; + streamRecord.xhttpSettings = xhttp; const xmux = xhttp.xmux; if (xmux && typeof xmux === 'object' && !Array.isArray(xmux)) { xhttp.enableXmux = true; diff --git a/frontend/src/lib/xray/stream-wire-normalize.ts b/frontend/src/lib/xray/stream-wire-normalize.ts index e74e450fb..fddb1c6a4 100644 --- a/frontend/src/lib/xray/stream-wire-normalize.ts +++ b/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): 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, keys: readonly string[]): void { for (const key of keys) { const v = obj[key]; @@ -160,6 +172,7 @@ export function normalizeXhttpForWire( side: StreamWireSide, ): Record { const out: Record = { ...raw }; + liftLegacyXhttpSessionKeys(out); const mode = typeof out.mode === 'string' && out.mode !== '' ? out.mode : 'auto'; const enableXmux = out.enableXmux === true; delete out.enableXmux; diff --git a/frontend/src/test/inbound-form-adapter.test.ts b/frontend/src/test/inbound-form-adapter.test.ts index 31a687d89..99750d8d6 100644 --- a/frontend/src/test/inbound-form-adapter.test.ts +++ b/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>).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>; + 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(); + }); +});