Files
3x-ui/internal/web/service/xray_xhttp_session_test.go
T
Rouzbeh† fea3c94b11 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.
2026-06-23 17:38:16 +02:00

82 lines
2.8 KiB
Go

package service
import (
"encoding/json"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
)
// xray-core v26.6.22 (#6258) renamed the XHTTP session keys with no fallback.
// The lift must rewrite stored configs at config-generation time so pre-upgrade
// inbounds/outbounds keep working without a manual re-save.
func TestLiftXhttpSessionIDKeys(t *testing.T) {
t.Run("lifts legacy keys and drops them", func(t *testing.T) {
stream := map[string]any{
"xhttpSettings": map[string]any{
"sessionPlacement": "cookie",
"sessionKey": "x_session",
},
}
if !liftXhttpSessionIDKeys(stream) {
t.Fatal("expected changed=true")
}
xhttp := stream["xhttpSettings"].(map[string]any)
if xhttp["sessionIDPlacement"] != "cookie" || xhttp["sessionIDKey"] != "x_session" {
t.Fatalf("renamed keys missing: %#v", xhttp)
}
if _, ok := xhttp["sessionPlacement"]; ok {
t.Fatal("legacy sessionPlacement still present")
}
if _, ok := xhttp["sessionKey"]; ok {
t.Fatal("legacy sessionKey still present")
}
})
t.Run("keeps an explicit new key over the legacy one", func(t *testing.T) {
stream := map[string]any{
"xhttpSettings": map[string]any{
"sessionPlacement": "cookie",
"sessionIDPlacement": "header",
},
}
liftXhttpSessionIDKeys(stream)
xhttp := stream["xhttpSettings"].(map[string]any)
if xhttp["sessionIDPlacement"] != "header" {
t.Fatalf("explicit new key was overwritten: %v", xhttp["sessionIDPlacement"])
}
})
t.Run("no-op without xhttpSettings or legacy keys", func(t *testing.T) {
if liftXhttpSessionIDKeys(map[string]any{"wsSettings": map[string]any{}}) {
t.Fatal("expected no change for non-xhttp stream")
}
if liftXhttpSessionIDKeys(map[string]any{"xhttpSettings": map[string]any{"path": "/"}}) {
t.Fatal("expected no change when no legacy keys present")
}
})
}
func TestLiftOutboundsXhttpSessionIDKeys(t *testing.T) {
raw := json_util.RawMessage(`[{"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"sessionKey":"x_session","sessionPlacement":"query"}}}]`)
out := liftOutboundsXhttpSessionIDKeys(raw)
var parsed []map[string]any
if err := json.Unmarshal(out, &parsed); err != nil {
t.Fatalf("unmarshal rewritten outbounds: %v", err)
}
xhttp := parsed[0]["streamSettings"].(map[string]any)["xhttpSettings"].(map[string]any)
if xhttp["sessionIDKey"] != "x_session" || xhttp["sessionIDPlacement"] != "query" {
t.Fatalf("outbound keys not lifted: %#v", xhttp)
}
if _, ok := xhttp["sessionKey"]; ok {
t.Fatal("legacy sessionKey survived in outbound")
}
// Unchanged input must return byte-identical output (no spurious hot-reload).
clean := json_util.RawMessage(`[{"protocol":"freedom"}]`)
if got := liftOutboundsXhttpSessionIDKeys(clean); string(got) != string(clean) {
t.Fatalf("clean outbounds were rewritten: %s", got)
}
}