diff --git a/common/lua/program.go b/common/lua/program.go index 57b950cee..23e9d5767 100644 --- a/common/lua/program.go +++ b/common/lua/program.go @@ -11,7 +11,7 @@ import ( "github.com/xtls/xray-core/common/base64" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" glua "github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua/parse" ) @@ -36,7 +36,7 @@ func Compile(script string) (*Program, error) { // TODO: Restore inline script support and re-enable the related tests. return nil, errors.New("currently unavailable") code, err = base64.Decode(script) - } else if name, err = platform.ResolveLuaFile(script); err == nil { + } else if name, err = filesystem.ResolveLuaFile(script); err == nil { code, err = os.ReadFile(name) // TODO: Restore privileged signature authorization and re-enable the related tests and CLI entries. // if err == nil { diff --git a/common/platform/filesystem/file.go b/common/platform/filesystem/file.go index f4f27e218..3d03e4b04 100644 --- a/common/platform/filesystem/file.go +++ b/common/platform/filesystem/file.go @@ -2,6 +2,7 @@ package filesystem import ( "errors" + "fmt" "io" "os" "path/filepath" @@ -61,7 +62,7 @@ func getAssetFileLocation(file string) (string, os.FileInfo, error) { if err != nil { return "", nil, err } - return platform.ResolveRegularFile(platform.GetAssetLocation(local)) + return ResolveRegularFile(platform.GetAssetLocation(local)) } func ReadCert(file string) ([]byte, error) { @@ -85,3 +86,59 @@ func CopyFile(dst string, src string) error { _, err = f.Write(bytes) return err } + +// ResolveLuaFile finds a local .lua script and returns its resolved absolute path. +// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. +func ResolveLuaFile(path string) (resolved string, err error) { + if filepath.IsAbs(path) { + resolved, _, err = ResolveRegularFile(path) + } else { + for _, dir := range []string{ + platform.GetConfDirPath(), + platform.NewEnvFlag(platform.ConfigLocation).GetValue(func() string { return "" }), + ".", + platform.GetExecutableDir(), + } { + if dir != "" { + if resolved, _, err = ResolveRegularFile(filepath.Join(dir, path)); err == nil || !errors.Is(err, os.ErrNotExist) { + break + } + } + } + } + if err != nil { + return "", err + } + if filepath.Ext(resolved) != ".lua" { + return "", errors.New("Lua file extension must be .lua") + } + return resolved, nil +} + +// ResolveRegularFile returns an absolute path and metadata of a regular file. +// File symlinks are resolved when possible; otherwise the original path is kept. +func ResolveRegularFile(path string) (string, os.FileInfo, error) { + path, err := filepath.Abs(path) + if err != nil { + return "", nil, fmt.Errorf("failed to resolve file path: %w", err) + } + info, err := os.Lstat(path) + if err != nil { + return "", nil, fmt.Errorf("failed to inspect file %q: %w", path, err) + } + if info.Mode()&os.ModeSymlink != 0 { + // Best effort: EvalSymlinks may fail when junctions are involved. + if resolved, err := filepath.EvalSymlinks(path); err == nil { + path = resolved + } + info, err = os.Stat(path) + if err != nil { + return "", nil, fmt.Errorf("failed to inspect file %q: %w", path, err) + } + } + if !info.Mode().IsRegular() { + return "", nil, fmt.Errorf("file is not a regular file: %s", path) + } + // Hard links are not handled. + return path, info, nil +} diff --git a/common/platform/filesystem/file_test.go b/common/platform/filesystem/file_test.go index 17da6e046..2b8bfbe7f 100644 --- a/common/platform/filesystem/file_test.go +++ b/common/platform/filesystem/file_test.go @@ -1,9 +1,15 @@ package filesystem_test import ( + "errors" + "os" "path/filepath" + "runtime" + "strings" "testing" + "github.com/xtls/xray-core/common" + "github.com/xtls/xray-core/common/platform" . "github.com/xtls/xray-core/common/platform/filesystem" ) @@ -30,3 +36,89 @@ func TestStatAssetRejectsInvalidPath(t *testing.T) { } } } + +func TestResolveLuaFile(t *testing.T) { + workingDir := t.TempDir() + t.Chdir(workingDir) + executable, err := os.Executable() + common.Must(err) + file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua") + common.Must(err) + common.Must(file.Close()) + defer os.Remove(file.Name()) + + name := filepath.Base(file.Name()) + paths := []string{ + filepath.Join(t.TempDir(), name), + filepath.Join(t.TempDir(), name), + filepath.Join(workingDir, name), + file.Name(), + } + t.Setenv(platform.ConfdirLocation, filepath.Dir(paths[0])) + t.Setenv(platform.ConfigLocation, filepath.Dir(paths[1])) + for _, path := range paths[:3] { + common.Must(os.WriteFile(path, nil, 0o600)) + } + for i, path := range paths { + paths[i], err = filepath.EvalSymlinks(path) + common.Must(err) + } + if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] { + t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2]) + } + for i, want := range paths { + if i == 2 { + t.Setenv(platform.ConfdirLocation, "") + t.Setenv(platform.ConfigLocation, "") + } + if got, err := ResolveLuaFile(name); err != nil || got != want { + t.Fatalf("resolved path = %q, %v; want %q", got, err, want) + } + common.Must(os.Remove(want)) + } + if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("missing file error = %v", err) + } + + t.Setenv(platform.ConfdirLocation, filepath.Dir(paths[0])) + t.Setenv(platform.ConfigLocation, filepath.Dir(paths[1])) + common.Must(os.Mkdir(paths[0], 0o700)) + common.Must(os.WriteFile(paths[1], nil, 0o600)) + for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} { + if _, err := ResolveLuaFile(path); err == nil { + t.Fatalf("accepted invalid path %q", path) + } + } +} + +func TestResolveLuaFileSymlink(t *testing.T) { + for _, tc := range []struct { + link, target string + valid bool + }{ + {"alias.lua", "data.dat", false}, + {"alias.dat", "script.lua", true}, + } { + t.Run(tc.link, func(t *testing.T) { + dir, err := filepath.EvalSymlinks(t.TempDir()) + common.Must(err) + target := filepath.Join(dir, tc.target) + common.Must(os.WriteFile(target, nil, 0o600)) + link := filepath.Join(dir, tc.link) + if err := os.Symlink(tc.target, link); err != nil { + if runtime.GOOS == "windows" { + t.Skipf("cannot create symlinks: %v", err) + } + t.Fatal(err) + } + got, err := ResolveLuaFile(link) + if tc.valid { + if err != nil || got != target { + t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target) + } + } else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") { + t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err) + } + }) + } +} diff --git a/common/platform/others.go b/common/platform/others.go index be86b6faa..f3e491d62 100644 --- a/common/platform/others.go +++ b/common/platform/others.go @@ -14,7 +14,7 @@ func LineSeparator() string { // GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations func GetAssetLocation(file string) string { - assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir) defPath := filepath.Join(assetPath, file) for _, p := range []string{ defPath, @@ -36,6 +36,6 @@ func GetAssetLocation(file string) string { // GetCertLocation searches for `file` in the env dir and the executable dir func GetCertLocation(file string) string { - certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) + certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir) return filepath.Join(certPath, file) } diff --git a/common/platform/platform.go b/common/platform/platform.go index 10e62b616..ce7c323db 100644 --- a/common/platform/platform.go +++ b/common/platform/platform.go @@ -1,8 +1,6 @@ package platform // import "github.com/xtls/xray-core/common/platform" import ( - "errors" - "fmt" "os" "path/filepath" "strconv" @@ -74,7 +72,7 @@ func NormalizeEnvName(name string) string { return strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(name)), ".", "_") } -func getExecutableDir() string { +func GetExecutableDir() string { exec, err := os.Executable() if err != nil { return "" @@ -83,7 +81,7 @@ func getExecutableDir() string { } func GetConfigurationPath() string { - configPath := NewEnvFlag(ConfigLocation).GetValue(getExecutableDir) + configPath := NewEnvFlag(ConfigLocation).GetValue(GetExecutableDir) return filepath.Join(configPath, "config.json") } @@ -92,59 +90,3 @@ func GetConfDirPath() string { configPath := NewEnvFlag(ConfdirLocation).GetValue(func() string { return "" }) return configPath } - -// ResolveLuaFile finds a local .lua script and returns its resolved absolute path. -// Relative paths: XRAY_LOCATION_CONFDIR > XRAY_LOCATION_CONFIG > working dir > executable dir. -func ResolveLuaFile(path string) (resolved string, err error) { - if filepath.IsAbs(path) { - resolved, _, err = ResolveRegularFile(path) - } else { - for _, dir := range []string{ - GetConfDirPath(), - NewEnvFlag(ConfigLocation).GetValue(func() string { return "" }), - ".", - getExecutableDir(), - } { - if dir != "" { - if resolved, _, err = ResolveRegularFile(filepath.Join(dir, path)); err == nil || !errors.Is(err, os.ErrNotExist) { - break - } - } - } - } - if err != nil { - return "", err - } - if filepath.Ext(resolved) != ".lua" { - return "", errors.New("Lua file extension must be .lua") - } - return resolved, nil -} - -// ResolveRegularFile returns an absolute path and metadata of a regular file. -// File symlinks are resolved when possible; otherwise the original path is kept. -func ResolveRegularFile(path string) (string, os.FileInfo, error) { - path, err := filepath.Abs(path) - if err != nil { - return "", nil, fmt.Errorf("failed to resolve file path: %w", err) - } - info, err := os.Lstat(path) - if err != nil { - return "", nil, fmt.Errorf("failed to inspect file %q: %w", path, err) - } - if info.Mode()&os.ModeSymlink != 0 { - // Best effort: EvalSymlinks may fail when junctions are involved. - if resolved, err := filepath.EvalSymlinks(path); err == nil { - path = resolved - } - info, err = os.Stat(path) - if err != nil { - return "", nil, fmt.Errorf("failed to inspect file %q: %w", path, err) - } - } - if !info.Mode().IsRegular() { - return "", nil, fmt.Errorf("file is not a regular file: %s", path) - } - // Hard links are not handled. - return path, info, nil -} diff --git a/common/platform/platform_test.go b/common/platform/platform_test.go index 87df1a195..854c397f6 100644 --- a/common/platform/platform_test.go +++ b/common/platform/platform_test.go @@ -1,11 +1,9 @@ package platform_test import ( - "errors" "os" "path/filepath" "runtime" - "strings" "testing" "github.com/xtls/xray-core/common" @@ -66,89 +64,3 @@ func TestGetAssetLocation(t *testing.T) { } } } - -func TestResolveLuaFile(t *testing.T) { - workingDir := t.TempDir() - t.Chdir(workingDir) - executable, err := os.Executable() - common.Must(err) - file, err := os.CreateTemp(filepath.Dir(executable), "lua-*.lua") - common.Must(err) - common.Must(file.Close()) - defer os.Remove(file.Name()) - - name := filepath.Base(file.Name()) - paths := []string{ - filepath.Join(t.TempDir(), name), - filepath.Join(t.TempDir(), name), - filepath.Join(workingDir, name), - file.Name(), - } - t.Setenv(ConfdirLocation, filepath.Dir(paths[0])) - t.Setenv(ConfigLocation, filepath.Dir(paths[1])) - for _, path := range paths[:3] { - common.Must(os.WriteFile(path, nil, 0o600)) - } - for i, path := range paths { - paths[i], err = filepath.EvalSymlinks(path) - common.Must(err) - } - if got, err := ResolveLuaFile(paths[2]); err != nil || got != paths[2] { - t.Fatalf("absolute path = %q, %v; want %q", got, err, paths[2]) - } - for i, want := range paths { - if i == 2 { - t.Setenv(ConfdirLocation, "") - t.Setenv(ConfigLocation, "") - } - if got, err := ResolveLuaFile(name); err != nil || got != want { - t.Fatalf("resolved path = %q, %v; want %q", got, err, want) - } - common.Must(os.Remove(want)) - } - if _, err := ResolveLuaFile(name); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("missing file error = %v", err) - } - - t.Setenv(ConfdirLocation, filepath.Dir(paths[0])) - t.Setenv(ConfigLocation, filepath.Dir(paths[1])) - common.Must(os.Mkdir(paths[0], 0o700)) - common.Must(os.WriteFile(paths[1], nil, 0o600)) - for _, path := range []string{"", name, filepath.Join(t.TempDir(), name)} { - if _, err := ResolveLuaFile(path); err == nil { - t.Fatalf("accepted invalid path %q", path) - } - } -} - -func TestResolveLuaFileSymlink(t *testing.T) { - for _, tc := range []struct { - link, target string - valid bool - }{ - {"alias.lua", "data.dat", false}, - {"alias.dat", "script.lua", true}, - } { - t.Run(tc.link, func(t *testing.T) { - dir, err := filepath.EvalSymlinks(t.TempDir()) - common.Must(err) - target := filepath.Join(dir, tc.target) - common.Must(os.WriteFile(target, nil, 0o600)) - link := filepath.Join(dir, tc.link) - if err := os.Symlink(tc.target, link); err != nil { - if runtime.GOOS == "windows" { - t.Skipf("cannot create symlinks: %v", err) - } - t.Fatal(err) - } - got, err := ResolveLuaFile(link) - if tc.valid { - if err != nil || got != target { - t.Fatalf("ResolveLuaFile(%q) = %q, %v; want %q", link, got, err, target) - } - } else if err == nil || got != "" || !strings.Contains(err.Error(), "Lua file extension must be .lua") { - t.Fatalf("ResolveLuaFile(%q) = %q, %v; want .lua extension error", link, got, err) - } - }) - } -} diff --git a/common/platform/windows.go b/common/platform/windows.go index 684ddc9c3..b6554b4b7 100644 --- a/common/platform/windows.go +++ b/common/platform/windows.go @@ -11,12 +11,12 @@ func LineSeparator() string { // GetAssetLocation searches for `file` in the env dir and the executable dir func GetAssetLocation(file string) string { - assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) + assetPath := NewEnvFlag(AssetLocation).GetValue(GetExecutableDir) return filepath.Join(assetPath, file) } // GetCertLocation searches for `file` in the env dir and the executable dir func GetCertLocation(file string) string { - certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir) + certPath := NewEnvFlag(CertLocation).GetValue(GetExecutableDir) return filepath.Join(certPath, file) } diff --git a/main/commands/all/lua/sign.go b/main/commands/all/lua/sign.go index b492d46dc..233bd9111 100644 --- a/main/commands/all/lua/sign.go +++ b/main/commands/all/lua/sign.go @@ -5,7 +5,7 @@ import ( "path/filepath" xlua "github.com/xtls/xray-core/common/lua" - "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/main/commands/base" ) @@ -33,7 +33,7 @@ func executeSign(cmd *base.Command, args []string) { cmd.Usage() } - input, info, err := platform.ResolveRegularFile(args[0]) + input, info, err := filesystem.ResolveRegularFile(args[0]) if err != nil { base.Fatalf("failed to inspect Lua file: %s", err) } @@ -67,7 +67,7 @@ func executeSign(cmd *base.Command, args []string) { } func writeSignedFile(path string, code []byte, mode os.FileMode) error { - if resolved, info, err := platform.ResolveRegularFile(path); err == nil { + if resolved, info, err := filesystem.ResolveRegularFile(path); err == nil { path = resolved mode = info.Mode().Perm() } else if !os.IsNotExist(err) { diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index f01dbadce..02ac400ee 100644 --- a/main/confloader/external/external.go +++ b/main/confloader/external/external.go @@ -14,7 +14,7 @@ import ( "github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/errors" - "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/main/commands/base" "github.com/xtls/xray-core/main/confloader" @@ -59,7 +59,7 @@ func readConfigFile(file *os.File) ([]byte, error) { return nil, err } if info.Mode().IsRegular() { - path, _, err := platform.ResolveRegularFile(file.Name()) + path, _, err := filesystem.ResolveRegularFile(file.Name()) if err != nil { return nil, err }