feat(sub): Add XHTTP session field compatibility in share links and subscriptions (#5929)

*  Add sessionKey and sessionPlacement compatability for previous clients

*  Add sessionKey and sessionPlacement compatability for previous clients on backend
This commit is contained in:
Maksim Alekseev
2026-07-28 23:15:28 +03:00
committed by GitHub
parent ff954ec48c
commit 041476a317
4 changed files with 119 additions and 2 deletions

View File

@@ -42,8 +42,7 @@ function xhttpHostFallback(xhttp: XHttpStreamSettings | undefined): string {
// Pull the bidirectional SplitHTTPConfig fields out of xhttp into a
// compact extra payload. Server-only fields (noSSEHeader, scMaxBufferedPosts,
// scStreamUpServerSecs, serverMaxHeaderBytes) are excluded — the client
// reading the share link wouldn't honor them. Mirrors the legacy
// Inbound.buildXhttpExtra exactly so the shadow link snapshots line up.
// reading the share link wouldn't honor them.
function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string, unknown> | null {
if (!xhttp) return null;
const extra: Record<string, unknown> = {};
@@ -85,6 +84,15 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string,
const v = xhttp[k];
if (typeof v === 'string' && v.length > 0 && v !== coreDefaults[k]) extra[k] = v;
}
// xray-core #6258 renamed these fields, but older clients still read the
// legacy names from share-link extra. Emit both names so one link works
// across old and new clients while the stored panel config stays canonical.
if (typeof extra.sessionIDPlacement === 'string') {
extra.sessionPlacement = extra.sessionIDPlacement;
}
if (typeof extra.sessionIDKey === 'string') {
extra.sessionKey = extra.sessionIDKey;
}
// Headers on the wire are a record; emit them as a map upstream's
// SplitHTTPConfig.headers expects, dropping Host (already on the URL).

View File

@@ -745,3 +745,73 @@ describe('genVlessLink flow gating (#5322)', () => {
expect(new URL(link).searchParams.get('flow')).toBe('xtls-rprx-vision');
});
});
describe('genVlessLink XHTTP extra compatibility', () => {
it('emits both sessionID and legacy session keys in XHTTP extra', () => {
const typed = InboundSchema.parse({
id: 1,
up: 0,
down: 0,
total: 0,
remark: 'xhttp-session',
enable: true,
expiryTime: 0,
listen: '',
port: 443,
tag: 'inbound-vless-xhttp',
sniffing: {
enabled: false,
destOverride: [],
metadataOnly: false,
routeOnly: false,
ipsExcluded: [],
domainsExcluded: [],
},
protocol: 'vless',
settings: {
clients: [
{
id: '11111111-2222-3333-4444-555555555555',
email: 'a@example.test',
flow: '',
limitIp: 0,
totalGB: 0,
expiryTime: 0,
enable: true,
tgId: 0,
subId: 's1',
comment: '',
reset: 0,
},
],
decryption: 'none',
encryption: 'none',
fallbacks: [],
},
streamSettings: {
network: 'xhttp',
security: 'none',
xhttpSettings: {
path: '/sp',
host: 'edge.example.test',
mode: 'auto',
sessionIDPlacement: 'header',
sessionIDKey: 'X-Session',
},
},
});
const link = genVlessLink({
inbound: typed,
address: 'example.test',
port: 443,
clientId: '11111111-2222-3333-4444-555555555555',
});
const extra = JSON.parse(new URL(link).searchParams.get('extra') ?? '{}') as Record<string, unknown>;
expect(extra.sessionIDPlacement).toBe('header');
expect(extra.sessionIDKey).toBe('X-Session');
expect(extra.sessionPlacement).toBe('header');
expect(extra.sessionKey).toBe('X-Session');
});
});