diff --git a/common/platform/platform.go b/common/platform/platform.go index 80e62874..b60b8bd2 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -17,6 +17,7 @@ const ( UseFreedomSplice = "xray.buf.splice" UseVmessPadding = "xray.vmess.padding" UseCone = "xray.cone.disabled" + UseStrictJSON = "xray.json.strict" BufferSize = "xray.ray.buffer.size" BrowserDialerAddress = "xray.browser.dialer" diff --git a/infra/conf/serial/builder.go b/infra/conf/serial/builder.go index 3ae98025..755b3469 100644 --- a/infra/conf/serial/builder.go +++ b/infra/conf/serial/builder.go @@ -5,12 +5,22 @@ import ( "io" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/platform" creflect "github.com/xtls/xray-core/common/reflect" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/main/confloader" ) +// UseStrictJSON, when true, makes JSON config decoders skip the custom +// comment-stripping reader and parse input as strict RFC 8259 JSON. +// +// Enabled by setting the env variable xray.json.strict=true (or its normalized +// form XRAY_JSON_STRICT=true). Default false preserves backward-compatible +// behavior for human-edited configs that may contain comments or other +// JSON5/JSONC syntax. +var UseStrictJSON = platform.NewEnvFlag(platform.UseStrictJSON).GetValue(func() string { return "" }) == "true" + func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) { c, err := mergeConfigs(files) if err != nil { @@ -31,7 +41,11 @@ func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) { if err != nil { return nil, errors.New("failed to read config: ", file).Base(err) } - c, err := ReaderDecoderByFormat[file.Format](r) + decoder := ReaderDecoderByFormat[file.Format] + if file.Format == "json" && UseStrictJSON { + decoder = DecodeJSONConfigStrict + } + c, err := decoder(r) if err != nil { return nil, errors.New("failed to decode config: ", file).Base(err) } diff --git a/infra/conf/serial/loader.go b/infra/conf/serial/loader.go index ef9963df..912f105c 100644 --- a/infra/conf/serial/loader.go +++ b/infra/conf/serial/loader.go @@ -42,6 +42,9 @@ func findOffset(b []byte, o int) *offset { // DecodeJSONConfig reads from reader and decode the config into *conf.Config // syntax error could be detected. +// +// Permissive: accepts JSON with Java/Python-style comments via json_reader.Reader. +// Used for local files and stdin where the config is human-edited. func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) { jsonConfig := &conf.Config{} @@ -69,6 +72,24 @@ func DecodeJSONConfig(reader io.Reader) (*conf.Config, error) { return jsonConfig, nil } +// DecodeJSONConfigStrict reads standard RFC 8259 JSON without comment-stripping. +// Used for remote sources (http/https/http+unix) where the payload is produced by +// automated systems and cannot contain JSON5/JSONC extensions. Avoids the +// byte-by-byte comment stripper and TeeReader, which are significant overhead on +// large configs. +func DecodeJSONConfigStrict(reader io.Reader) (*conf.Config, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, errors.New("failed to read config file").Base(err) + } + jsonConfig := &conf.Config{} + if err := json.Unmarshal(data, jsonConfig); err != nil { + return nil, errors.New("failed to parse remote JSON config").Base(err) + } + return jsonConfig, nil +} + + func LoadJSONConfig(reader io.Reader) (*core.Config, error) { jsonConfig, err := DecodeJSONConfig(reader) if err != nil { diff --git a/main/json/json.go b/main/json/json.go index aad2dc6b..ca64e9d0 100644 --- a/main/json/json.go +++ b/main/json/json.go @@ -41,6 +41,13 @@ func init() { } return cf.Build() case io.Reader: + if serial.UseStrictJSON { + cfg, err := serial.DecodeJSONConfigStrict(v) + if err != nil { + return nil, err + } + return cfg.Build() + } return serial.LoadJSONConfig(v) default: return nil, errors.New("unknown type")