Compare commits

...

2 Commits

Author SHA1 Message Date
Ona
70307c7cba Document quoted include support in README
- Add examples of quoted include paths
- Explain relative path resolution behavior
- Mention compatibility with tools like Gitpod

Co-authored-by: Ona <no-reply@ona.com>
2025-09-26 00:16:21 +00:00
Ona
94407289db Add support for quoted values in SSH config
- Support both double quotes and single quotes in config values
- Handle quoted Include directives (e.g., Include "gitpod/config")
- Properly resolve relative paths in includes to ~/.ssh/ directory
- Maintain compatibility with unquoted values
- Add parseConfigLine function for proper quote handling

This fixes compatibility with Gitpod and other tools that generate
SSH configs with quoted include paths.

Co-authored-by: Ona <no-reply@ona.com>
2025-09-26 00:16:10 +00:00
2 changed files with 38 additions and 6 deletions

View File

@@ -88,8 +88,11 @@ kport supports the SSH `Include` directive, allowing you to organize your SSH co
- **Glob patterns**: `Include ~/.ssh/config.d/*`
- **Specific files**: `Include ~/.ssh/work-config`
- **Relative paths**: `Include config.d/servers`
- **Quoted paths**: `Include "gitpod/config"` or `Include 'path with spaces/config'`
- **Cycle detection**: Prevents infinite loops from circular includes
Relative paths in includes are resolved relative to `~/.ssh/` directory, matching OpenSSH behavior.
## Authentication
The application supports:

View File

@@ -77,14 +77,11 @@ func (sc *SSHConfig) loadConfigFromFileRecursive(path string, visited map[string
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
key, value, err := parseConfigLine(line)
if err != nil {
continue // Skip malformed lines
}
key := strings.ToLower(parts[0])
value := strings.Join(parts[1:], " ")
switch key {
case "include":
// Handle include directive
@@ -142,6 +139,13 @@ func (sc *SSHConfig) processInclude(pattern string, visited map[string]bool) err
return fmt.Errorf("failed to get home directory: %w", err)
}
pattern = filepath.Join(homeDir, pattern[2:])
} else if !filepath.IsAbs(pattern) {
// Relative paths are relative to ~/.ssh/
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
pattern = filepath.Join(homeDir, ".ssh", pattern)
}
// Handle glob patterns
@@ -179,4 +183,29 @@ func (sc *SSHConfig) GetHostByName(name string) (*SSHHost, error) {
}
}
return nil, fmt.Errorf("host '%s' not found", name)
}
// parseConfigLine parses a SSH config line, handling quoted values
func parseConfigLine(line string) (key, value string, err error) {
// Find the first whitespace to separate key from value
parts := strings.SplitN(strings.TrimSpace(line), " ", 2)
if len(parts) < 2 {
return "", "", fmt.Errorf("invalid config line")
}
key = strings.ToLower(strings.TrimSpace(parts[0]))
valueStr := strings.TrimSpace(parts[1])
// Handle quoted values
if len(valueStr) >= 2 &&
((valueStr[0] == '"' && valueStr[len(valueStr)-1] == '"') ||
(valueStr[0] == '\'' && valueStr[len(valueStr)-1] == '\'')) {
// Remove quotes
value = valueStr[1 : len(valueStr)-1]
} else {
// Handle unquoted values (may contain multiple words)
value = valueStr
}
return key, value, nil
}