mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-10 05:10:58 +00:00
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:
@@ -42,8 +42,7 @@ function xhttpHostFallback(xhttp: XHttpStreamSettings | undefined): string {
|
|||||||
// Pull the bidirectional SplitHTTPConfig fields out of xhttp into a
|
// Pull the bidirectional SplitHTTPConfig fields out of xhttp into a
|
||||||
// compact extra payload. Server-only fields (noSSEHeader, scMaxBufferedPosts,
|
// compact extra payload. Server-only fields (noSSEHeader, scMaxBufferedPosts,
|
||||||
// scStreamUpServerSecs, serverMaxHeaderBytes) are excluded — the client
|
// scStreamUpServerSecs, serverMaxHeaderBytes) are excluded — the client
|
||||||
// reading the share link wouldn't honor them. Mirrors the legacy
|
// reading the share link wouldn't honor them.
|
||||||
// Inbound.buildXhttpExtra exactly so the shadow link snapshots line up.
|
|
||||||
function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string, unknown> | null {
|
function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string, unknown> | null {
|
||||||
if (!xhttp) return null;
|
if (!xhttp) return null;
|
||||||
const extra: Record<string, unknown> = {};
|
const extra: Record<string, unknown> = {};
|
||||||
@@ -85,6 +84,15 @@ function buildXhttpExtra(xhttp: XHttpStreamSettings | undefined): Record<string,
|
|||||||
const v = xhttp[k];
|
const v = xhttp[k];
|
||||||
if (typeof v === 'string' && v.length > 0 && v !== coreDefaults[k]) extra[k] = v;
|
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
|
// Headers on the wire are a record; emit them as a map upstream's
|
||||||
// SplitHTTPConfig.headers expects, dropping Host (already on the URL).
|
// SplitHTTPConfig.headers expects, dropping Host (already on the URL).
|
||||||
|
|||||||
@@ -745,3 +745,73 @@ describe('genVlessLink flow gating (#5322)', () => {
|
|||||||
expect(new URL(link).searchParams.get('flow')).toBe('xtls-rprx-vision');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2017,6 +2017,15 @@ func buildXhttpExtra(xhttp map[string]any) map[string]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Older clients still read the pre-#6258 names from the subscription
|
||||||
|
// extra JSON. Emit aliases after lifting legacy inputs so both old and
|
||||||
|
// new clients can consume the same link.
|
||||||
|
if v, ok := extra["sessionIDPlacement"].(string); ok && len(v) > 0 {
|
||||||
|
extra["sessionPlacement"] = v
|
||||||
|
}
|
||||||
|
if v, ok := extra["sessionIDKey"].(string); ok && len(v) > 0 {
|
||||||
|
extra["sessionKey"] = v
|
||||||
|
}
|
||||||
|
|
||||||
for _, field := range []string{"uplinkChunkSize"} {
|
for _, field := range []string{"uplinkChunkSize"} {
|
||||||
if v, ok := nonZeroShareValue(xhttp[field]); ok {
|
if v, ok := nonZeroShareValue(xhttp[field]); ok {
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ func TestBuildXhttpExtra_IncludesClientSideFieldsWhenPresent(t *testing.T) {
|
|||||||
"mode": "packet-up",
|
"mode": "packet-up",
|
||||||
"xPaddingBytes": "100-1000",
|
"xPaddingBytes": "100-1000",
|
||||||
"uplinkHTTPMethod": "GET",
|
"uplinkHTTPMethod": "GET",
|
||||||
|
"sessionIDPlacement": "header",
|
||||||
|
"sessionIDKey": "X-Session",
|
||||||
"uplinkChunkSize": float64(4096),
|
"uplinkChunkSize": float64(4096),
|
||||||
"noGRPCHeader": true,
|
"noGRPCHeader": true,
|
||||||
"scMinPostsIntervalMs": "20-40",
|
"scMinPostsIntervalMs": "20-40",
|
||||||
@@ -375,6 +377,16 @@ func TestBuildXhttpExtra_IncludesClientSideFieldsWhenPresent(t *testing.T) {
|
|||||||
if extra["mode"] != "packet-up" {
|
if extra["mode"] != "packet-up" {
|
||||||
t.Fatalf("extra[mode] = %#v, want packet-up", extra["mode"])
|
t.Fatalf("extra[mode] = %#v, want packet-up", extra["mode"])
|
||||||
}
|
}
|
||||||
|
for key, want := range map[string]string{
|
||||||
|
"sessionIDPlacement": "header",
|
||||||
|
"sessionIDKey": "X-Session",
|
||||||
|
"sessionPlacement": "header",
|
||||||
|
"sessionKey": "X-Session",
|
||||||
|
} {
|
||||||
|
if extra[key] != want {
|
||||||
|
t.Fatalf("extra[%s] = %#v, want %q; extra %#v", key, extra[key], want, extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
headers, ok := extra["headers"].(map[string]any)
|
headers, ok := extra["headers"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -388,6 +400,24 @@ func TestBuildXhttpExtra_IncludesClientSideFieldsWhenPresent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildXhttpExtra_LegacySessionFieldsEmitBothNames(t *testing.T) {
|
||||||
|
extra := buildXhttpExtra(map[string]any{
|
||||||
|
"sessionPlacement": "query",
|
||||||
|
"sessionKey": "sess",
|
||||||
|
})
|
||||||
|
|
||||||
|
for key, want := range map[string]string{
|
||||||
|
"sessionIDPlacement": "query",
|
||||||
|
"sessionIDKey": "sess",
|
||||||
|
"sessionPlacement": "query",
|
||||||
|
"sessionKey": "sess",
|
||||||
|
} {
|
||||||
|
if extra[key] != want {
|
||||||
|
t.Fatalf("extra[%s] = %#v, want %q; extra %#v", key, extra[key], want, extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildXhttpExtra_LeavesDefaultClientSideFieldsOut(t *testing.T) {
|
func TestBuildXhttpExtra_LeavesDefaultClientSideFieldsOut(t *testing.T) {
|
||||||
extra := buildXhttpExtra(map[string]any{
|
extra := buildXhttpExtra(map[string]any{
|
||||||
"uplinkHTTPMethod": "",
|
"uplinkHTTPMethod": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user