Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpwnnex <pwnnex@proton.me>2026-04-21 21:30:02 +0300
committerGitHub <noreply@github.com>2026-04-21 21:30:02 +0300
commit15be803da982ab362f6859c0ec91a582c4b1fea6 (patch)
treea4e716380ede864b97ef98b6dccb05926ffec32f /web/service/xray_setting_test.go
parentc79b45e512bd41d24f668a373fdabb922c437877 (diff)
Fix blank Xray Settings page from wrapped xrayTemplateConfig (#4059) (#4069)
`getXraySetting` builds its response as { "xraySetting": <db value>, "inboundTags": ..., "outboundTestUrl": ... } and embeds the raw DB value as the `xraySetting` field without checking whether the stored value already has that exact shape. The frontend pulls the textarea content from `result.xraySetting` and saves it back verbatim. If the DB ever ends up holding the response-shaped wrapper instead of a real xray config (older installs where this happened at least once, users who imported a copy-pasted response into the textarea, a botched migration, etc.), the next save nests another layer, the one after that nests a third, and the Vue-side JSON.parse of the resulting blob silently fails — the Xray Settings page goes blank. Fix both ends of the round-trip: * Add `service.UnwrapXrayTemplateConfig`. It peels off any number of `xraySetting`-keyed layers, leaving a real xray config behind. The check is conservative: if the outer object already contains any top-level xray key (`inbounds`, `outbounds`, `routing`, `api`, `dns`, `log`, `policy`, `stats`), it is returned unchanged, and there is a depth cap to avoid pathological inputs. * `SaveXraySetting` unwraps before validation so a round-tripped wrapper from an already-corrupted page can no longer re-poison the DB on save. * `getXraySetting` unwraps on read and, when it finds a wrapper, rewrites the DB with the corrected value. Existing broken installs heal themselves on the next visit to the page. Includes unit tests for the passthrough, single-wrap, multi-wrap, string-encoded-inner, and false-positive cases. Co-authored-by: pwnnex <eternxles@gmail.com>
Diffstat (limited to 'web/service/xray_setting_test.go')
-rw-r--r--web/service/xray_setting_test.go90
1 files changed, 90 insertions, 0 deletions
diff --git a/web/service/xray_setting_test.go b/web/service/xray_setting_test.go
new file mode 100644
index 00000000..2c165576
--- /dev/null
+++ b/web/service/xray_setting_test.go
@@ -0,0 +1,90 @@
+package service
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+)
+
+func TestUnwrapXrayTemplateConfig(t *testing.T) {
+ real := `{"log":{},"inbounds":[],"outbounds":[],"routing":{}}`
+
+ t.Run("passes through a clean config", func(t *testing.T) {
+ if got := UnwrapXrayTemplateConfig(real); got != real {
+ t.Fatalf("clean config was modified: %s", got)
+ }
+ })
+
+ t.Run("passes through invalid JSON unchanged", func(t *testing.T) {
+ in := "not json at all"
+ if got := UnwrapXrayTemplateConfig(in); got != in {
+ t.Fatalf("invalid input was modified: %s", got)
+ }
+ })
+
+ t.Run("unwraps one layer of response-shaped wrapper", func(t *testing.T) {
+ wrapper := `{"inboundTags":["tag"],"outboundTestUrl":"x","xraySetting":` + real + `}`
+ got := UnwrapXrayTemplateConfig(wrapper)
+ if !equalJSON(t, got, real) {
+ t.Fatalf("want %s, got %s", real, got)
+ }
+ })
+
+ t.Run("unwraps multiple stacked layers", func(t *testing.T) {
+ lvl1 := `{"xraySetting":` + real + `}`
+ lvl2 := `{"xraySetting":` + lvl1 + `}`
+ lvl3 := `{"xraySetting":` + lvl2 + `}`
+ got := UnwrapXrayTemplateConfig(lvl3)
+ if !equalJSON(t, got, real) {
+ t.Fatalf("want %s, got %s", real, got)
+ }
+ })
+
+ t.Run("handles an xraySetting stored as a JSON-encoded string", func(t *testing.T) {
+ encoded, _ := json.Marshal(real) // becomes a quoted string
+ wrapper := `{"xraySetting":` + string(encoded) + `}`
+ got := UnwrapXrayTemplateConfig(wrapper)
+ if !equalJSON(t, got, real) {
+ t.Fatalf("want %s, got %s", real, got)
+ }
+ })
+
+ t.Run("does not unwrap when top level already has real xray keys", func(t *testing.T) {
+ // Pathological but defensible: if a user's actual config somehow
+ // has both the real keys and an unrelated `xraySetting` key, we
+ // must not strip it.
+ in := `{"inbounds":[],"xraySetting":{"some":"thing"}}`
+ got := UnwrapXrayTemplateConfig(in)
+ if got != in {
+ t.Fatalf("should have left real config alone, got %s", got)
+ }
+ })
+
+ t.Run("stops at a reasonable depth", func(t *testing.T) {
+ // Build a deeper-than-maxDepth chain that ends at something
+ // non-wrapped, and confirm we end up at some valid JSON (we
+ // don't loop forever and we don't blow the stack).
+ s := real
+ for i := 0; i < 16; i++ {
+ s = `{"xraySetting":` + s + `}`
+ }
+ got := UnwrapXrayTemplateConfig(s)
+ if !strings.Contains(got, `"inbounds"`) && !strings.Contains(got, `"xraySetting"`) {
+ t.Fatalf("unexpected tail: %s", got)
+ }
+ })
+}
+
+func equalJSON(t *testing.T, a, b string) bool {
+ t.Helper()
+ var va, vb any
+ if err := json.Unmarshal([]byte(a), &va); err != nil {
+ return false
+ }
+ if err := json.Unmarshal([]byte(b), &vb); err != nil {
+ return false
+ }
+ ja, _ := json.Marshal(va)
+ jb, _ := json.Marshal(vb)
+ return string(ja) == string(jb)
+}