From 87f8fb9c6c24cc166b1c7dfff91da734868345e0 Mon Sep 17 00:00:00 2001 From: Gu1llaum-3 Date: Sun, 4 Jan 2026 22:21:13 +0100 Subject: [PATCH] fix: problems with quotes from Host names and support SSH tokens (issue #32) --- internal/config/ssh.go | 7 +++ internal/config/ssh_test.go | 86 +++++++++++++++++++++++++++++++++ internal/validation/ssh.go | 14 +++++- internal/validation/ssh_test.go | 13 +++++ 4 files changed, 119 insertions(+), 1 deletion(-) diff --git a/internal/config/ssh.go b/internal/config/ssh.go index 9b13d50..5d95d83 100644 --- a/internal/config/ssh.go +++ b/internal/config/ssh.go @@ -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! diff --git a/internal/config/ssh_test.go b/internal/config/ssh_test.go index 1229249..d6c36ff 100644 --- a/internal/config/ssh_test.go +++ b/internal/config/ssh_test.go @@ -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) + } + } +} diff --git a/internal/validation/ssh.go b/internal/validation/ssh.go index 07c24aa..93bee63 100644 --- a/internal/validation/ssh.go +++ b/internal/validation/ssh.go @@ -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) } diff --git a/internal/validation/ssh_test.go b/internal/validation/ssh_test.go index 673838c..32dca0c 100644 --- a/internal/validation/ssh_test.go +++ b/internal/validation/ssh_test.go @@ -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 {