Ver Fonte

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

* :sparkles: Add sessionKey and sessionPlacement compatability for previous clients

* :sparkles: Add sessionKey and sessionPlacement compatability for previous clients on backend
Maksim Alekseev há 8 horas atrás
pai
commit
041476a317

+ 10 - 2
frontend/src/lib/xray/inbound-link.ts

@@ -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).

+ 70 - 0
frontend/src/test/inbound-link.test.ts

@@ -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: '[email protected]',
+            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');
+  });
+});

+ 9 - 0
internal/sub/service.go

@@ -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"} {
 		if v, ok := nonZeroShareValue(xhttp[field]); ok {

+ 30 - 0
internal/sub/service_test.go

@@ -335,6 +335,8 @@ func TestBuildXhttpExtra_IncludesClientSideFieldsWhenPresent(t *testing.T) {
 		"mode":                 "packet-up",
 		"xPaddingBytes":        "100-1000",
 		"uplinkHTTPMethod":     "GET",
+		"sessionIDPlacement":   "header",
+		"sessionIDKey":         "X-Session",
 		"uplinkChunkSize":      float64(4096),
 		"noGRPCHeader":         true,
 		"scMinPostsIntervalMs": "20-40",
@@ -375,6 +377,16 @@ func TestBuildXhttpExtra_IncludesClientSideFieldsWhenPresent(t *testing.T) {
 	if extra["mode"] != "packet-up" {
 		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)
 	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) {
 	extra := buildXhttpExtra(map[string]any{
 		"uplinkHTTPMethod": "",