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
This commit is contained in:
MHSanaei
2026-07-01 23:11:58 +02:00
parent 273f88721e
commit 539bcc897c
3 changed files with 66 additions and 2 deletions
@@ -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();
});
});