mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-22 20:46:08 +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:
@@ -121,6 +121,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
xrayConfig.API = ensureAPIServices(xrayConfig.API)
|
||||
xrayConfig.Policy = ensureStatsPolicy(xrayConfig.Policy)
|
||||
xrayConfig.RouterConfig = stripDisabledRules(xrayConfig.RouterConfig)
|
||||
// Template outbounds authored before the xray-core #6258 XHTTP rename may
|
||||
// still carry sessionPlacement/sessionKey; lift them too (same reason as
|
||||
// the per-inbound lift below).
|
||||
xrayConfig.OutboundConfigs = liftOutboundsXhttpSessionIDKeys(xrayConfig.OutboundConfigs)
|
||||
|
||||
_, _, _ = s.inboundService.AddTraffic(nil, nil)
|
||||
|
||||
@@ -251,6 +255,12 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
|
||||
delete(stream, "externalProxy")
|
||||
|
||||
// xray-core v26.6.22 (#6258) renamed the XHTTP session keys and
|
||||
// kept no fallback. Lift legacy sessionPlacement/sessionKey onto the
|
||||
// new names here so inbounds stored before the rename keep working
|
||||
// without the admin re-saving them.
|
||||
liftXhttpSessionIDKeys(stream)
|
||||
|
||||
newStream, err := json.MarshalIndent(stream, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -576,7 +586,7 @@ func mergeSubscriptionOutbounds(cfg *xray.Config, prepend, appendList []any) {
|
||||
return
|
||||
}
|
||||
}
|
||||
merged := make([]any, 0, len(prepend)+len(templateOutbounds)+len(appendList))
|
||||
var merged []any
|
||||
merged = append(merged, prepend...)
|
||||
merged = append(merged, templateOutbounds...)
|
||||
merged = append(merged, appendList...)
|
||||
@@ -1078,3 +1088,60 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
||||
func (s *XrayService) DidXrayCrash() bool {
|
||||
return !s.IsXrayRunning() && !isManuallyStopped.Load()
|
||||
}
|
||||
|
||||
// liftXhttpSessionIDKeys renames the legacy XHTTP session keys
|
||||
// (sessionPlacement/sessionKey) to the v26.6.22 #6258 names
|
||||
// (sessionIDPlacement/sessionIDKey) inside a streamSettings map. xray-core kept
|
||||
// no fallback for the old names, so a config stored before the rename would be
|
||||
// silently ignored by the engine. Returns true if it changed anything.
|
||||
func liftXhttpSessionIDKeys(stream map[string]any) bool {
|
||||
xhttp, ok := stream["xhttpSettings"].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
changed := false
|
||||
for legacy, renamed := range map[string]string{
|
||||
"sessionPlacement": "sessionIDPlacement",
|
||||
"sessionKey": "sessionIDKey",
|
||||
} {
|
||||
v, has := xhttp[legacy]
|
||||
if !has {
|
||||
continue
|
||||
}
|
||||
if _, exists := xhttp[renamed]; !exists {
|
||||
xhttp[renamed] = v
|
||||
}
|
||||
delete(xhttp, legacy)
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// liftOutboundsXhttpSessionIDKeys applies liftXhttpSessionIDKeys to every
|
||||
// outbound's streamSettings in the raw outbounds array. The original bytes are
|
||||
// returned untouched when nothing needs lifting, so an unchanged config never
|
||||
// looks modified to the hot-reload diff.
|
||||
func liftOutboundsXhttpSessionIDKeys(raw json_util.RawMessage) json_util.RawMessage {
|
||||
if len(raw) == 0 {
|
||||
return raw
|
||||
}
|
||||
var outbounds []map[string]any
|
||||
if err := json.Unmarshal(raw, &outbounds); err != nil {
|
||||
return raw
|
||||
}
|
||||
changed := false
|
||||
for _, ob := range outbounds {
|
||||
if stream, ok := ob["streamSettings"].(map[string]any); ok {
|
||||
if liftXhttpSessionIDKeys(stream) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return raw
|
||||
}
|
||||
if rewritten, err := json.Marshal(outbounds); err == nil {
|
||||
return rewritten
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user