fix: problems with quotes from Host names and support SSH tokens (issue #32)

This commit is contained in:
2026-01-04 22:21:13 +01:00
parent 8f780e288c
commit 87f8fb9c6c
4 changed files with 119 additions and 1 deletions

View File

@@ -283,8 +283,12 @@ func parseSSHConfigFileWithProcessedFiles(configPath string, processedFiles map[
hostNames := strings.Fields(value)
// Skip hosts with wildcards (*, ?) as they are typically patterns, not actual hosts
// Also remove surrounding quotes from host names
var validHostNames []string
for _, hostName := range hostNames {
// Remove surrounding double quotes if present
hostName = strings.Trim(hostName, `"`)
if !strings.ContainsAny(hostName, "*?") {
validHostNames = append(validHostNames, hostName)
}
@@ -896,6 +900,9 @@ func quickHostSearchInFile(hostName string, configPath string, processedFiles ma
// Check if our target host is in this Host declaration
for _, candidateHostName := range hostNames {
// Remove surrounding double quotes if present
candidateHostName = strings.Trim(candidateHostName, `"`)
// Skip hosts with wildcards (*, ?) as they are typically patterns
if !strings.ContainsAny(candidateHostName, "*?") && candidateHostName == hostName {
return true, nil // Found the host!

View File

@@ -1694,3 +1694,89 @@ Host production-server
}
}
}
func TestParseSSHConfigWithQuotedHostNames(t *testing.T) {
tempDir := t.TempDir()
configFile := filepath.Join(tempDir, "config")
configContent := `# Test hosts with quoted names (issue #32)
Host "my-host-name-01"
HostName my-host-name-01.cwd.pub.domain.net
Port 2222
User my_user
Host "qa-test-vm"
HostName qa-test-vm.example.com
User guillaume
Port 22
Host normal-host
HostName normal.example.com
User testuser
Host "quoted1" "quoted2"
HostName multi.example.com
User multiuser
`
err := os.WriteFile(configFile, []byte(configContent), 0600)
if err != nil {
t.Fatalf("Failed to create config: %v", err)
}
hosts, err := ParseSSHConfigFile(configFile)
if err != nil {
t.Fatalf("ParseSSHConfigFile() error = %v", err)
}
// Should get 5 hosts: my-host-name-01, qa-test-vm, normal-host, quoted1, quoted2
// All without quotes
expectedHosts := map[string]struct{}{
"my-host-name-01": {},
"qa-test-vm": {},
"normal-host": {},
"quoted1": {},
"quoted2": {},
}
if len(hosts) != len(expectedHosts) {
t.Errorf("Expected %d hosts, got %d", len(expectedHosts), len(hosts))
for _, host := range hosts {
t.Logf("Found host: %q", host.Name)
}
}
hostMap := make(map[string]SSHHost)
for _, host := range hosts {
// Verify no quotes in host names
if strings.Contains(host.Name, `"`) {
t.Errorf("Host name %q still contains quotes", host.Name)
}
hostMap[host.Name] = host
}
for expectedHostName := range expectedHosts {
if _, found := hostMap[expectedHostName]; !found {
t.Errorf("Expected host %q not found", expectedHostName)
}
}
// Verify specific host details
if host, found := hostMap["my-host-name-01"]; found {
if host.Hostname != "my-host-name-01.cwd.pub.domain.net" {
t.Errorf("Host my-host-name-01 has wrong hostname: %q", host.Hostname)
}
if host.Port != "2222" {
t.Errorf("Host my-host-name-01 has wrong port: %q", host.Port)
}
if host.User != "my_user" {
t.Errorf("Host my-host-name-01 has wrong user: %q", host.User)
}
}
if host, found := hostMap["qa-test-vm"]; found {
if host.Hostname != "qa-test-vm.example.com" {
t.Errorf("Host qa-test-vm has wrong hostname: %q", host.Hostname)
}
}
}

View File

@@ -11,6 +11,7 @@ import (
)
// ValidateHostname checks if a hostname is valid
// Accepts regular hostnames, IP addresses, and SSH tokens like %h, %p, %r, %u, %n, %C, %d, %i, %k, %L, %l, %T
func ValidateHostname(hostname string) bool {
if len(hostname) == 0 || len(hostname) > 253 {
return false
@@ -22,7 +23,18 @@ func ValidateHostname(hostname string) bool {
return false
}
hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$`)
// Check if hostname contains SSH tokens (e.g., %h, %p, %r, %u, %n, etc.)
// SSH tokens are documented in ssh_config(5) man page
sshTokenRegex := regexp.MustCompile(`%[hprunCdiklLT]`)
if sshTokenRegex.MatchString(hostname) {
// If it contains SSH tokens, it's a valid SSH config construct
return true
}
// RFC 1123: each label must start with alphanumeric, end with alphanumeric,
// and contain only alphanumeric and hyphens. Labels are 1-63 chars.
// A hostname is one or more labels separated by dots.
hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9]{0,62})?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9]{0,62})?)*$`)
return hostnameRegex.MatchString(hostname)
}

View File

@@ -24,6 +24,19 @@ func TestValidateHostname(t *testing.T) {
{"hostname ending with dot", "example.com.", false},
{"hostname with hyphen", "my-server.com", true},
{"hostname starting with number", "1example.com", true},
{"multiple hyphens and subdomains", "my-host-name-01.cwd.pub.domain.net", true},
{"multiple hyphens", "my-host-name-01", true},
{"complex hostname with hyphens", "server-01-prod.data-center.example.com", true},
{"hostname with consecutive hyphens", "my--server.com", true},
{"single char labels", "a.b.c.d.com", true},
// SSH tokens support (issue #32 comment)
{"SSH token %h", "%h.server.com", true},
{"SSH token %p", "server.com:%p", true},
{"SSH token %r", "%r@server.com", true},
{"SSH token %u", "%u.example.com", true},
{"SSH token %n", "%n.domain.net", true},
{"SSH token %C", "host-%C.com", true},
{"multiple SSH tokens", "%h.%u.server.com", true},
}
for _, tt := range tests {