Add support for SSH config includes
- Support Include directive in SSH config files - Handle glob patterns (e.g., Include ~/.ssh/config.d/*) - Prevent circular includes with cycle detection - Gracefully handle missing or invalid include files - Maintain compatibility with existing config parsing This allows users to organize their SSH configs across multiple files as supported by OpenSSH. Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
@@ -42,9 +42,27 @@ func (sc *SSHConfig) LoadConfig() error {
|
|||||||
|
|
||||||
// LoadConfigFromFile loads SSH configuration from a specific file
|
// LoadConfigFromFile loads SSH configuration from a specific file
|
||||||
func (sc *SSHConfig) LoadConfigFromFile(path string) error {
|
func (sc *SSHConfig) LoadConfigFromFile(path string) error {
|
||||||
|
return sc.loadConfigFromFileRecursive(path, make(map[string]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfigFromFileRecursive loads SSH config with include support and cycle detection
|
||||||
|
func (sc *SSHConfig) loadConfigFromFileRecursive(path string, visited map[string]bool) error {
|
||||||
|
// Resolve absolute path to detect cycles
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve path %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cycles
|
||||||
|
if visited[absPath] {
|
||||||
|
return fmt.Errorf("circular include detected: %s", absPath)
|
||||||
|
}
|
||||||
|
visited[absPath] = true
|
||||||
|
defer delete(visited, absPath)
|
||||||
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open SSH config file: %w", err)
|
return fmt.Errorf("failed to open SSH config file %s: %w", path, err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
@@ -68,6 +86,12 @@ func (sc *SSHConfig) LoadConfigFromFile(path string) error {
|
|||||||
value := strings.Join(parts[1:], " ")
|
value := strings.Join(parts[1:], " ")
|
||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
|
case "include":
|
||||||
|
// Handle include directive
|
||||||
|
if err := sc.processInclude(value, visited); err != nil {
|
||||||
|
// Log error but continue processing
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to process include %s: %v\n", value, err)
|
||||||
|
}
|
||||||
case "host":
|
case "host":
|
||||||
// Save previous host if exists
|
// Save previous host if exists
|
||||||
if currentHost != nil {
|
if currentHost != nil {
|
||||||
@@ -103,7 +127,40 @@ func (sc *SSHConfig) LoadConfigFromFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return fmt.Errorf("error reading SSH config file: %w", err)
|
return fmt.Errorf("error reading SSH config file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processInclude handles SSH config include directives
|
||||||
|
func (sc *SSHConfig) processInclude(pattern string, visited map[string]bool) error {
|
||||||
|
// Expand tilde to home directory
|
||||||
|
if strings.HasPrefix(pattern, "~/") {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get home directory: %w", err)
|
||||||
|
}
|
||||||
|
pattern = filepath.Join(homeDir, pattern[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle glob patterns
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid glob pattern %s: %w", pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each matching file
|
||||||
|
for _, match := range matches {
|
||||||
|
// Skip directories
|
||||||
|
if info, err := os.Stat(match); err == nil && info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively load the included file
|
||||||
|
if err := sc.loadConfigFromFileRecursive(match, visited); err != nil {
|
||||||
|
return fmt.Errorf("failed to load included file %s: %w", match, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
Reference in New Issue
Block a user