mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 22:26:09 +00:00
feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506)
* feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) xray-core v26.6.22 (PR #6258) renamed the XHTTP session config keys sessionPlacement/sessionKey to sessionIDPlacement/sessionIDKey (no fallback kept in core) and added sessionIDTable (predefined charset name or literal ASCII) and sessionIDLength (range, e.g. 16-32, lower bound > 0). Panel changes: - Schema (xhttp.ts): rename the two keys, add sessionIDTable/sessionIDLength, and a z.preprocess that lifts legacy keys off stored configs so an upgraded panel never silently drops a saved session setting. - Wire normalize + share-link build/parse: rename keys, emit the two new fields, and accept legacy sessionPlacement/sessionKey from old share links. - Inbound + outbound XHTTP forms: rename field paths, add a sessionIDTable autocomplete (9 predefined tables + free ASCII) and a sessionIDLength range input shown only when a table is set, with light client validation (ASCII table, length min > 0; xray enforces the room-size minimum server-side). - Subscription (service.go) and Clash (clash_service.go) builders: emit the renamed + new keys, with a legacy fallback for not-yet-resaved inbounds. - Locales: add sessionIDTable/sessionIDLength labels + hints in all 13 files. Two sibling v26.6.22 XHTTP commits need no panel change and are covered by the core bump alone: #6332 (XHTTP/3 closes QUIC/UDP) and #6320 (udpHop honors the existing dialerProxy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(xhttp): add Session ID Table to inbound form-blocks snapshot The new sessionIDTable input renders by default in the inbound XHTTP form, so its label joins the field-structure snapshot. sessionIDLength stays conditional (only shown when a table is set), so it does not appear here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(xhttp): migrate legacy session keys in the running xray config The Zod preprocess plus the subscription/Clash fallbacks only covered the panel UI and share-link output. The config handed to the running xray-core process is built from the raw stored streamSettings in GetXrayConfig, which did not rewrite the renamed XHTTP session keys — so a pre-upgrade inbound (or template outbound) stored with a non-default sessionPlacement was emitted unchanged and dropped by xray-core v26.6.22, until the admin re-saved it. Lift sessionPlacement/sessionKey onto sessionIDPlacement/sessionIDKey at config-generation time, in the existing inbound stream-rewrite block (next to the tls/reality/externalProxy handling) and across template outbounds. The lift is idempotent and leaves unchanged configs byte-identical so the hot-reload diff never sees a spurious change. Also tighten validateSessionIDLength to reject an inverted range (e.g. 32-16) in addition to the existing lower-bound > 0 check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(xray): avoid summed-capacity allocation in mergeSubscriptionOutbounds CodeQL go/allocation-size-overflow flagged the pre-sized make() whose capacity was a sum of three slice lengths. Grow the slice via append on a nil slice instead; same result, no overflow-prone capacity expression.
This commit is contained in:
@@ -118,6 +118,7 @@ exports[`inbound transport forms > XhttpForm field structure is stable 1`] = `
|
||||
"Uplink HTTP Method",
|
||||
"Padding Obfs Mode",
|
||||
"Session Placement",
|
||||
"Session ID Table",
|
||||
"Sequence Placement",
|
||||
"No SSE Header",
|
||||
"XMUX",
|
||||
|
||||
@@ -53,8 +53,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = `
|
||||
"seqKey": "",
|
||||
"seqPlacement": "",
|
||||
"serverMaxHeaderBytes": 0,
|
||||
"sessionKey": "",
|
||||
"sessionPlacement": "",
|
||||
"sessionIDKey": "",
|
||||
"sessionIDLength": "",
|
||||
"sessionIDPlacement": "",
|
||||
"sessionIDTable": "",
|
||||
"uplinkChunkSize": 0,
|
||||
"uplinkDataKey": "",
|
||||
"uplinkDataPlacement": "",
|
||||
@@ -87,8 +89,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-padding byte-stably
|
||||
"seqKey": "",
|
||||
"seqPlacement": "",
|
||||
"serverMaxHeaderBytes": 0,
|
||||
"sessionKey": "",
|
||||
"sessionPlacement": "",
|
||||
"sessionIDKey": "",
|
||||
"sessionIDLength": "",
|
||||
"sessionIDPlacement": "",
|
||||
"sessionIDTable": "",
|
||||
"uplinkChunkSize": 0,
|
||||
"uplinkDataKey": "",
|
||||
"uplinkDataPlacement": "",
|
||||
@@ -121,8 +125,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-placement byte-stab
|
||||
"seqKey": "X-Seq",
|
||||
"seqPlacement": "cookie",
|
||||
"serverMaxHeaderBytes": 0,
|
||||
"sessionKey": "X-Session",
|
||||
"sessionPlacement": "header",
|
||||
"sessionIDKey": "X-Session",
|
||||
"sessionIDLength": "",
|
||||
"sessionIDPlacement": "header",
|
||||
"sessionIDTable": "",
|
||||
"uplinkChunkSize": 0,
|
||||
"uplinkDataKey": "u",
|
||||
"uplinkDataPlacement": "query",
|
||||
@@ -158,8 +164,10 @@ exports[`NetworkSettingsSchema fixtures > parses xhttp-extra-tuning byte-stably
|
||||
"seqKey": "",
|
||||
"seqPlacement": "",
|
||||
"serverMaxHeaderBytes": 16384,
|
||||
"sessionKey": "",
|
||||
"sessionPlacement": "",
|
||||
"sessionIDKey": "",
|
||||
"sessionIDLength": "",
|
||||
"sessionIDPlacement": "",
|
||||
"sessionIDTable": "",
|
||||
"uplinkChunkSize": 8192,
|
||||
"uplinkDataKey": "",
|
||||
"uplinkDataPlacement": "",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"path": "/sp",
|
||||
"host": "edge.example.test",
|
||||
"mode": "auto",
|
||||
"sessionPlacement": "header",
|
||||
"sessionKey": "X-Session",
|
||||
"sessionIDPlacement": "header",
|
||||
"sessionIDKey": "X-Session",
|
||||
"seqPlacement": "cookie",
|
||||
"seqKey": "X-Seq",
|
||||
"uplinkDataPlacement": "query",
|
||||
|
||||
@@ -93,6 +93,7 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
|
||||
scMaxBufferedPosts: 50,
|
||||
tls: 'tls',
|
||||
};
|
||||
// legacy sessionKey must alias onto the renamed sessionIDKey (#6258)
|
||||
const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
|
||||
const out = parseVmessLink(link);
|
||||
const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
|
||||
@@ -101,7 +102,8 @@ describe('parseVmessLink — XHTTP advanced fields', () => {
|
||||
expect(xhttp.xPaddingHeader).toBe('X-Pad');
|
||||
expect(xhttp.xPaddingPlacement).toBe('header');
|
||||
expect(xhttp.xPaddingMethod).toBe('random');
|
||||
expect(xhttp.sessionKey).toBe('X-Session');
|
||||
expect(xhttp.sessionIDKey).toBe('X-Session');
|
||||
expect(xhttp.sessionKey).toBeUndefined();
|
||||
expect(xhttp.seqKey).toBe('X-Seq');
|
||||
expect(xhttp.noSSEHeader).toBe(true);
|
||||
expect(xhttp.scMaxBufferedPosts).toBe(50);
|
||||
@@ -135,7 +137,8 @@ describe('parseVlessLink — XHTTP advanced fields', () => {
|
||||
+ '?type=xhttp&security=tls&host=edge.example&path=%2Fsp'
|
||||
+ '&xPaddingObfsMode=true&xPaddingKey=secret-key&xPaddingHeader=X-Pad'
|
||||
+ '&xPaddingPlacement=header&xPaddingMethod=random'
|
||||
+ '&sessionKey=X-Session&seqKey=X-Seq&noSSEHeader=true'
|
||||
+ '&sessionIDKey=X-Session&sessionIDTable=Base62&sessionIDLength=16-32'
|
||||
+ '&seqKey=X-Seq&noSSEHeader=true'
|
||||
+ '&scMaxBufferedPosts=50'
|
||||
+ '#imported-pad';
|
||||
const out = parseVlessLink(link);
|
||||
@@ -145,7 +148,9 @@ describe('parseVlessLink — XHTTP advanced fields', () => {
|
||||
expect(xhttp.xPaddingHeader).toBe('X-Pad');
|
||||
expect(xhttp.xPaddingPlacement).toBe('header');
|
||||
expect(xhttp.xPaddingMethod).toBe('random');
|
||||
expect(xhttp.sessionKey).toBe('X-Session');
|
||||
expect(xhttp.sessionIDKey).toBe('X-Session');
|
||||
expect(xhttp.sessionIDTable).toBe('Base62');
|
||||
expect(xhttp.sessionIDLength).toBe('16-32');
|
||||
expect(xhttp.seqKey).toBe('X-Seq');
|
||||
expect(xhttp.noSSEHeader).toBe(true);
|
||||
expect(xhttp.scMaxBufferedPosts).toBe(50);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
|
||||
import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
|
||||
|
||||
// xray-core #6258: sessionPlacement/sessionKey were renamed to
|
||||
// sessionIDPlacement/sessionIDKey. The schema must lift legacy keys off
|
||||
// stored configs so an upgraded panel never silently drops them.
|
||||
describe('XHttpStreamSettingsSchema legacy migration', () => {
|
||||
it('lifts legacy sessionPlacement/sessionKey onto the renamed keys', () => {
|
||||
const parsed = XHttpStreamSettingsSchema.parse({
|
||||
sessionPlacement: 'cookie',
|
||||
sessionKey: 'x_session',
|
||||
});
|
||||
expect(parsed.sessionIDPlacement).toBe('cookie');
|
||||
expect(parsed.sessionIDKey).toBe('x_session');
|
||||
// legacy keys must not survive — we never emit both names
|
||||
expect((parsed as Record<string, unknown>).sessionPlacement).toBeUndefined();
|
||||
expect((parsed as Record<string, unknown>).sessionKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prefers an explicit new key over a legacy one', () => {
|
||||
const parsed = XHttpStreamSettingsSchema.parse({
|
||||
sessionPlacement: 'cookie',
|
||||
sessionIDPlacement: 'header',
|
||||
});
|
||||
expect(parsed.sessionIDPlacement).toBe('header');
|
||||
});
|
||||
|
||||
it('defaults the new fields to empty', () => {
|
||||
const parsed = XHttpStreamSettingsSchema.parse({});
|
||||
expect(parsed.sessionIDTable).toBe('');
|
||||
expect(parsed.sessionIDLength).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionID validators', () => {
|
||||
it('accepts empty and ASCII tables, rejects non-ASCII', async () => {
|
||||
await expect(validateSessionIDTable(null, '')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDTable(null, 'Base62')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDTable(null, 'ABCdef0123')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDTable(null, ' café')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('accepts a positive length/range, rejects zero or junk', async () => {
|
||||
await expect(validateSessionIDLength(null, '')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDLength(null, '8')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDLength(null, '16-32')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDLength(null, '8-8')).resolves.toBeUndefined();
|
||||
await expect(validateSessionIDLength(null, '0-16')).rejects.toThrow();
|
||||
await expect(validateSessionIDLength(null, '32-16')).rejects.toThrow();
|
||||
await expect(validateSessionIDLength(null, 'abc')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user