diff --git a/core/config.go b/core/config.go index 112035782..199105bc1 100644 --- a/core/config.go +++ b/core/config.go @@ -175,9 +175,13 @@ func init() { switch v := input.(type) { case cmdarg.Arg: r, err := confloader.LoadConfig(v[0]) - common.Must(err) + if err != nil { + return nil, err + } data, err := buf.ReadAllToBytes(r) - common.Must(err) + if err != nil { + return nil, err + } return loadProtobufConfig(data) case io.Reader: data, err := buf.ReadAllToBytes(v) diff --git a/main/commands/base/env.go b/main/commands/base/env.go index 27ba0b1c3..9d431479e 100644 --- a/main/commands/base/env.go +++ b/main/commands/base/env.go @@ -4,6 +4,8 @@ package base type CommandEnvHolder struct { // Executable name of current binary Exec string + // Full name of the command being executed + CommandName string // commands column width of current command CommandsWidth int } diff --git a/main/commands/base/execute.go b/main/commands/base/execute.go index 8e23dabfa..32844781a 100644 --- a/main/commands/base/execute.go +++ b/main/commands/base/execute.go @@ -61,6 +61,7 @@ BigCmdLoop: } buildCommandText(cmd) + CommandEnv.CommandName = cmdName cmd.Run(cmd, args) Exit() return diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index 02066109b..1aa6dacf4 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -8,12 +8,14 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "time" "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/utils" + "github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/confloader" ) @@ -28,10 +30,15 @@ func ConfigLoader(arg string) (out io.Reader, err error) { data, err = FetchHTTPContent(arg) case arg == "stdin:": - data, err = io.ReadAll(os.Stdin) + data, err = readConfigFile(os.Stdin) default: - data, err = os.ReadFile(arg) + file, openErr := os.Open(arg) + if openErr != nil { + return nil, openErr + } + defer file.Close() + data, err = readConfigFile(file) } if err != nil { @@ -41,6 +48,33 @@ func ConfigLoader(arg string) (out io.Reader, err error) { return } +func readConfigFile(file *os.File) ([]byte, error) { + if base.CommandEnv.CommandName != "run" { + return io.ReadAll(file) + } + + info, err := file.Stat() + if err != nil { + return nil, err + } + if info.Mode().IsRegular() { + path, err := filepath.Abs(file.Name()) + if err != nil { + return nil, err + } + path, err = filepath.EvalSymlinks(path) + if err != nil { + return nil, err + } + switch strings.ToLower(filepath.Ext(path)) { + case ".jsonc", ".json", ".yml", ".yaml", ".toml", ".pb", ".protobuf": + default: + return nil, errors.New("unsupported config file extension: ", path) + } + } + return io.ReadAll(file) +} + // FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL // or a Unix socket HTTP endpoint. // diff --git a/main/run_test.go b/main/run_test.go new file mode 100644 index 000000000..75640463c --- /dev/null +++ b/main/run_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "flag" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestRunConfigCommands(t *testing.T) { + if os.Getenv("XRAY_TEST_CONFIG_COMMAND") == "1" { + os.Args = append([]string{os.Args[0]}, flag.Args()...) + main() + return + } + dir := t.TempDir() + config := filepath.Join(dir, "config.yaml") + script := filepath.Join(dir, "config.lua") + binary := filepath.Join(dir, "config.pb") + message := filepath.Join(dir, "user.tmsg") + for path, body := range map[string]string{ + config: "{}", + script: "{}", + binary: "", + message: `{"type":"xray.app.dispatcher.Config","value":""}`, + } { + if err := os.WriteFile(path, []byte(body), 0o600); err != nil { + t.Fatal(err) + } + } + file, err := os.Open(script) + if err != nil { + t.Fatal(err) + } + defer file.Close() + for _, tc := range []struct { + name string + args []string + code int + input io.Reader + }{ + {"format override", []string{"run", "-test", "-format", "json", "-c", config}, 0, nil}, + {"invalid extension", []string{"run", "-test", "-format", "json", "-c", script}, 23, nil}, + {"default command", []string{"-test", "-format", "json", "-c", script}, 23, nil}, + {"protobuf config", []string{"run", "-test", "-c", binary}, 0, nil}, + {"protobuf error", []string{"run", "-test", "-format", "protobuf", "-c", script}, 23, nil}, + {"merged source", []string{"run", "-test", "-format", "json", "-c", config, "-c", script}, 23, nil}, + {"dump", []string{"run", "-dump", "-c", config}, 0, nil}, + {"stdin pipe", []string{"run", "-test", "-c", "stdin:"}, 0, nil}, + {"stdin file", []string{"run", "-test", "-c", "stdin:"}, 23, file}, + {"convert json", []string{"convert", "json", message}, 0, nil}, + {"convert pb", []string{"convert", "pb", "-debug", config}, 0, nil}, + {"convert pb stdin", []string{"convert", "pb", "-debug", "stdin:"}, 0, file}, + } { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + args := append([]string{"-test.run=^TestRunConfigCommands$", "--"}, tc.args...) + cmd := exec.CommandContext(ctx, os.Args[0], args...) + cmd.Env = append(os.Environ(), "XRAY_TEST_CONFIG_COMMAND=1") + cmd.Stdin = strings.NewReader("{}") + if tc.input != nil { + cmd.Stdin = tc.input + } + output, err := cmd.CombinedOutput() + if cmd.ProcessState == nil { + t.Fatal(err) + } + if code := cmd.ProcessState.ExitCode(); code != tc.code { + t.Fatalf("exit code = %d, want %d: %v\n%s", code, tc.code, err, output) + } + if tc.code == 23 && !strings.Contains(string(output), "failed to load config files") { + t.Fatalf("missing config error:\n%s", output) + } + }) + } +}