xray_setting.go 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. package service
  2. import (
  3. _ "embed"
  4. "encoding/json"
  5. "github.com/mhsanaei/3x-ui/v2/util/common"
  6. "github.com/mhsanaei/3x-ui/v2/xray"
  7. )
  8. // XraySettingService provides business logic for Xray configuration management.
  9. // It handles validation and storage of Xray template configurations.
  10. type XraySettingService struct {
  11. SettingService
  12. }
  13. func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
  14. // The frontend round-trips the whole getXraySetting response back
  15. // through the textarea, so if it has ever received a wrapped
  16. // payload (see UnwrapXrayTemplateConfig) it sends that same wrapper
  17. // back here. Strip it before validation/storage, otherwise we save
  18. // garbage the next read can't recover from without this same call.
  19. newXraySettings = UnwrapXrayTemplateConfig(newXraySettings)
  20. if err := s.CheckXrayConfig(newXraySettings); err != nil {
  21. return err
  22. }
  23. return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings)
  24. }
  25. func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
  26. xrayConfig := &xray.Config{}
  27. err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig)
  28. if err != nil {
  29. return common.NewError("xray template config invalid:", err)
  30. }
  31. return nil
  32. }
  33. // UnwrapXrayTemplateConfig returns the raw xray config JSON from `raw`,
  34. // peeling off any number of `{ "inboundTags": ..., "outboundTestUrl": ...,
  35. // "xraySetting": <real config> }` response-shaped wrappers that may have
  36. // ended up in the database.
  37. //
  38. // How it got there: getXraySetting used to embed the raw DB value as
  39. // `xraySetting` in its response without checking whether the stored
  40. // value was already that exact response shape. If the frontend then
  41. // saved it verbatim (the textarea is a round-trip of the JSON it was
  42. // handed), the wrapper got persisted — and each subsequent save nested
  43. // another layer, producing the blank Xray Settings page reported in
  44. // issue #4059.
  45. //
  46. // If `raw` does not look like a wrapper, it is returned unchanged.
  47. func UnwrapXrayTemplateConfig(raw string) string {
  48. const maxDepth = 8 // defensive cap against pathological multi-nest values
  49. for i := 0; i < maxDepth; i++ {
  50. var top map[string]json.RawMessage
  51. if err := json.Unmarshal([]byte(raw), &top); err != nil {
  52. return raw
  53. }
  54. inner, ok := top["xraySetting"]
  55. if !ok {
  56. return raw
  57. }
  58. // Real xray configs never contain a top-level "xraySetting" key,
  59. // but they do contain things like "inbounds"/"outbounds"/"api".
  60. // If any of those are present, we're already at the real config
  61. // and the "xraySetting" field is either user data or coincidence
  62. // — don't touch it.
  63. for _, k := range []string{"inbounds", "outbounds", "routing", "api", "dns", "log", "policy", "stats"} {
  64. if _, hit := top[k]; hit {
  65. return raw
  66. }
  67. }
  68. // Peel off one layer.
  69. unwrapped := string(inner)
  70. // `xraySetting` may be stored either as a JSON object or as a
  71. // JSON-encoded string of an object. Handle both.
  72. var asStr string
  73. if err := json.Unmarshal(inner, &asStr); err == nil {
  74. unwrapped = asStr
  75. }
  76. raw = unwrapped
  77. }
  78. return raw
  79. }