mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-25 06:42:52 +08:00
fix(sdk/go): accept CLI-compatible gateway names (#4767)
* fix(sdk/go): accept CLI-compatible gateway names Signed-off-by: Sunil Kumar Vadlamani <25136458+sunilteja93@users.noreply.github.com> * fix(sdk/go): enforce Windows-safe gateway names Signed-off-by: Sunil Kumar Vadlamani <25136458+sunilteja93@users.noreply.github.com> --------- Signed-off-by: Sunil Kumar Vadlamani <25136458+sunilteja93@users.noreply.github.com> Co-authored-by: Sunil Kumar Vadlamani <25136458+sunilteja93@users.noreply.github.com>
This commit is contained in:
co-authored by
Sunil Kumar Vadlamani
parent
72ab4dd9ef
commit
837fdfdc51
@@ -8,7 +8,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -91,32 +90,50 @@ func resolveGatewayDir(name string) (string, ConfigSource, error) {
|
||||
return "", "", fmt.Errorf("%w: %q", ErrGatewayNotFound, name)
|
||||
}
|
||||
|
||||
// validateGatewayName checks that a gateway name is safe for use as a
|
||||
// directory component. It rejects:
|
||||
// - empty names
|
||||
// - names containing path separators (/ or \)
|
||||
// - names that are "." or ".." (directory traversal)
|
||||
// - names containing "." (prevents hidden files and extension confusion)
|
||||
// - names with characters outside ASCII alphanumerics, dashes, underscores
|
||||
// validateGatewayName accepts portable, single-component gateway names.
|
||||
// Host-derived FQDN and IPv4 names may contain dots. Windows device names,
|
||||
// reserved filename characters, and ambiguous dot components are rejected
|
||||
// on every platform to keep SDK behavior consistent.
|
||||
func validateGatewayName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("%w: name must not be empty", ErrInvalidGatewayName)
|
||||
}
|
||||
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
if name == "." || name == ".." {
|
||||
return fmt.Errorf("%w: invalid path component", ErrInvalidGatewayName)
|
||||
}
|
||||
if strings.ContainsAny(name, `/\`) {
|
||||
return fmt.Errorf("%w: name must not contain path separators", ErrInvalidGatewayName)
|
||||
}
|
||||
|
||||
if strings.Contains(name, ".") {
|
||||
return fmt.Errorf("%w: name must not contain dots", ErrInvalidGatewayName)
|
||||
if strings.ContainsRune(name, '\x00') {
|
||||
return fmt.Errorf("%w: name must not contain NUL", ErrInvalidGatewayName)
|
||||
}
|
||||
if strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") || strings.Contains(name, "..") {
|
||||
return fmt.Errorf("%w: invalid dot placement", ErrInvalidGatewayName)
|
||||
}
|
||||
|
||||
// This ASCII-only grammar excludes Windows reserved filename characters
|
||||
// (including colons in raw IPv6 addresses), spaces, and control characters.
|
||||
for _, r := range name {
|
||||
if !isValidNameRune(r) {
|
||||
return fmt.Errorf("%w: name contains invalid character %q", ErrInvalidGatewayName, string(r))
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%w: non-portable character %q", ErrInvalidGatewayName, r)
|
||||
}
|
||||
|
||||
// Windows treats device names as reserved even with an extension:
|
||||
// e.g. CON.txt and COM1.example.com refer to device names.
|
||||
stem, _, _ := strings.Cut(name, ".")
|
||||
stem = strings.ToUpper(stem)
|
||||
switch stem {
|
||||
case "CON", "PRN", "AUX", "NUL":
|
||||
return fmt.Errorf("%w: reserved Windows device name", ErrInvalidGatewayName)
|
||||
}
|
||||
// Reject COM0/LPT0 conservatively too, for Windows portability.
|
||||
if len(stem) == 4 && (strings.HasPrefix(stem, "COM") || strings.HasPrefix(stem, "LPT")) &&
|
||||
stem[3] >= '0' && stem[3] <= '9' {
|
||||
return fmt.Errorf("%w: reserved Windows device name", ErrInvalidGatewayName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,15 +184,3 @@ func listGatewayDirs(base string) ([]string, error) {
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// isValidNameRune returns true if the rune is an ASCII letter, digit,
|
||||
// dash, or underscore.
|
||||
func isValidNameRune(r rune) bool {
|
||||
if r > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
return (r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9') ||
|
||||
r == '-' || r == '_'
|
||||
}
|
||||
|
||||
@@ -113,6 +113,15 @@ func TestValidateGatewayName_ValidNames(t *testing.T) {
|
||||
"a",
|
||||
"test-gateway-01",
|
||||
"A_B_C",
|
||||
"gw.example.com",
|
||||
"192.0.2.1",
|
||||
"foo.bar",
|
||||
"config.json",
|
||||
"gw-1.example.com",
|
||||
"COM10",
|
||||
"LPT10",
|
||||
"CONSOLE",
|
||||
"com1x.example.com",
|
||||
}
|
||||
|
||||
for _, name := range validNames {
|
||||
@@ -147,13 +156,10 @@ func TestValidateGatewayName_PathSeparators(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGatewayName_Dots(t *testing.T) {
|
||||
func TestValidateGatewayName_DotComponents(t *testing.T) {
|
||||
cases := []string{
|
||||
".",
|
||||
"..",
|
||||
".hidden",
|
||||
"foo.bar",
|
||||
"config.json",
|
||||
}
|
||||
|
||||
for _, name := range cases {
|
||||
@@ -220,14 +226,80 @@ func TestValidateGatewayName_SpecialCharacters(t *testing.T) {
|
||||
{"foo#bar", "hash"},
|
||||
{"café", "non-ASCII"},
|
||||
{"日本語", "unicode"},
|
||||
{"foo bar", "tab (space)"},
|
||||
{"foo\tbar", "tab"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := validateGatewayName(tc.name)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidGatewayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Windows filename rules apply regardless of the host operating system.
|
||||
func TestValidateGatewayName_WindowsFilenameRules(t *testing.T) {
|
||||
invalid := []string{
|
||||
"CON", "con.txt", "CON.example.com",
|
||||
"PRN", "prn.log", "AUX", "aux.json", "NUL", "nul.config",
|
||||
"COM0", "COM1", "com9.txt", "COM1.example.com",
|
||||
"LPT0", "LPT1", "lpt9.log",
|
||||
"gateway.", ".hidden", "foo..bar", "gw.example.com.",
|
||||
"gateway ", "foo bar", "foo:stream", "C:gateway",
|
||||
"foo|bar", "foo?bar", "foo*bar", "foo<bar", "foo>bar", "foo\"bar",
|
||||
"gw\x01bar", "[2001:db8::1]", "2001:db8::1",
|
||||
}
|
||||
for _, name := range invalid {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := validateGatewayName(name)
|
||||
assert.ErrorIs(t, err, ErrInvalidGatewayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostDerivedGatewayNames(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", tmp)
|
||||
t.Setenv(systemGatewayDirEnv, filepath.Join(tmp, "system"))
|
||||
|
||||
names := []string{"gw.example.com", "192.0.2.1"}
|
||||
for _, name := range names {
|
||||
dir := filepath.Join(tmp, "openshell", "gateways", name)
|
||||
require.NoError(t, os.MkdirAll(dir, 0o755))
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(dir, "metadata.json"),
|
||||
[]byte(`{"gateway_endpoint":"https://gw.example.com:443","auth_mode":"none"}`),
|
||||
0o600,
|
||||
))
|
||||
|
||||
cfg, err := LoadConfig(name)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, name, cfg.Name)
|
||||
}
|
||||
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(tmp, "openshell", "active_gateway"),
|
||||
[]byte("gw.example.com"), 0o600,
|
||||
))
|
||||
|
||||
active, err := LoadConfig("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "gw.example.com", active.Name)
|
||||
|
||||
gateways, err := ListGateways()
|
||||
require.NoError(t, err)
|
||||
got := make(map[string]bool)
|
||||
for _, gw := range gateways {
|
||||
got[gw.Name] = gw.Active
|
||||
}
|
||||
assert.Equal(t, map[string]bool{
|
||||
"gw.example.com": true,
|
||||
"192.0.2.1": false,
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestValidateGatewayName_NUL(t *testing.T) {
|
||||
err := validateGatewayName("gw\x00example")
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidGatewayName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user