mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-05 04:14:21 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user