// Copyright 2025. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ssh_config_file import ( "strconv" "strings" "time" "github.com/Adembc/lazyssh/internal/core/domain" "github.com/kevinburke/ssh_config" ) // toDomainServer converts ssh_config.Config to a slice of domain.Server. func (r *Repository) toDomainServer(cfg *ssh_config.Config) []domain.Server { servers := make([]domain.Server, 0, len(cfg.Hosts)) for _, host := range cfg.Hosts { aliases := make([]string, 0, len(host.Patterns)) for _, pattern := range host.Patterns { alias := pattern.String() // Skip if alias contains wildcards (not a concrete Host) if strings.ContainsAny(alias, "!*?[]") { continue } aliases = append(aliases, alias) } if len(aliases) == 0 { continue } server := domain.Server{ Alias: aliases[0], Aliases: aliases, Port: 22, IdentityFiles: []string{}, } for _, node := range host.Nodes { kvNode, ok := node.(*ssh_config.KV) if !ok { continue } r.mapKVToServer(&server, kvNode) } servers = append(servers, server) } return servers } // mapKVToServer maps an ssh_config.KV node to the corresponding fields in domain.Server. func (r *Repository) mapKVToServer(server *domain.Server, kvNode *ssh_config.KV) { key := strings.ToLower(kvNode.Key) value := kvNode.Value // Try mapping in order of categories if r.mapBasicConfig(server, key, value) { return } if r.mapConnectionConfig(server, key, value) { return } if r.mapForwardingConfig(server, key, value) { return } if r.mapAuthenticationConfig(server, key, value) { return } if r.mapSecurityConfig(server, key, value) { return } if r.mapEnvironmentConfig(server, key, value) { return } r.mapDebugConfig(server, key, value) } // mapBasicConfig maps basic SSH configuration fields func (r *Repository) mapBasicConfig(server *domain.Server, key, value string) bool { switch key { case "hostname": server.Host = value case "user": server.User = value case "port": port, err := strconv.Atoi(value) if err == nil { server.Port = port } case "identityfile": server.IdentityFiles = append(server.IdentityFiles, value) default: return false } return true } // mapConnectionConfig maps connection and proxy configuration fields func (r *Repository) mapConnectionConfig(server *domain.Server, key, value string) bool { switch key { case "proxycommand": server.ProxyCommand = value case "proxyjump": server.ProxyJump = value case "remotecommand": server.RemoteCommand = value case "requesttty": server.RequestTTY = value case "sessiontype": server.SessionType = value case "connecttimeout": server.ConnectTimeout = value case "connectionattempts": server.ConnectionAttempts = value case "bindaddress": server.BindAddress = value case "bindinterface": server.BindInterface = value case "addressfamily": server.AddressFamily = value case "exitonforwardfailure": server.ExitOnForwardFailure = value case "ipqos": server.IPQoS = value case "canonicalizehostname": server.CanonicalizeHostname = value case "canonicaldomains": server.CanonicalDomains = value case "canonicalizefallbacklocal": server.CanonicalizeFallbackLocal = value case "canonicalizemaxdots": server.CanonicalizeMaxDots = value case "canonicalizepermittedcnames": server.CanonicalizePermittedCNAMEs = value case "serveraliveinterval": server.ServerAliveInterval = value case "serveralivecountmax": server.ServerAliveCountMax = value case "compression": server.Compression = value case "tcpkeepalive": server.TCPKeepAlive = value case "batchmode": server.BatchMode = value case "controlmaster": server.ControlMaster = value case "controlpath": server.ControlPath = value case "controlpersist": server.ControlPersist = value default: return false } return true } // mapForwardingConfig maps port forwarding and agent forwarding fields func (r *Repository) mapForwardingConfig(server *domain.Server, key, value string) bool { switch key { case "localforward": cliFormat := r.convertConfigForwardToCLIFormat(value) server.LocalForward = append(server.LocalForward, cliFormat) case "remoteforward": cliFormat := r.convertConfigForwardToCLIFormat(value) server.RemoteForward = append(server.RemoteForward, cliFormat) case "dynamicforward": server.DynamicForward = append(server.DynamicForward, value) case "clearallforwardings": server.ClearAllForwardings = value case "gatewayports": server.GatewayPorts = value case "forwardagent": server.ForwardAgent = value case "forwardx11": server.ForwardX11 = value case "forwardx11trusted": server.ForwardX11Trusted = value default: return false } return true } // mapAuthenticationConfig maps authentication-related fields func (r *Repository) mapAuthenticationConfig(server *domain.Server, key, value string) bool { switch key { case "pubkeyauthentication": server.PubkeyAuthentication = value case "pubkeyacceptedalgorithms", "pubkeyacceptedkeytypes": // PubkeyAcceptedKeyTypes is deprecated alias for PubkeyAcceptedAlgorithms (since OpenSSH 8.5) server.PubkeyAcceptedAlgorithms = value case "hostbasedacceptedalgorithms", "hostbasedkeytypes", "hostbasedacceptedkeytypes": // HostbasedKeyTypes and HostbasedAcceptedKeyTypes are deprecated aliases (since OpenSSH 8.5) server.HostbasedAcceptedAlgorithms = value case "passwordauthentication": server.PasswordAuthentication = value case "preferredauthentications": server.PreferredAuthentications = value case "identitiesonly": server.IdentitiesOnly = value case "addkeystoagent": server.AddKeysToAgent = value case "identityagent": server.IdentityAgent = value case "kbdinteractiveauthentication", "challengeresponseauthentication": // ChallengeResponseAuthentication is deprecated alias for KbdInteractiveAuthentication server.KbdInteractiveAuthentication = value case "numberofpasswordprompts": server.NumberOfPasswordPrompts = value default: return false } return true } // mapSecurityConfig maps security-related fields func (r *Repository) mapSecurityConfig(server *domain.Server, key, value string) bool { switch key { case "stricthostkeychecking": server.StrictHostKeyChecking = value case "checkhostip": server.CheckHostIP = value case "fingerprinthash": server.FingerprintHash = value case "userknownhostsfile": server.UserKnownHostsFile = value case "hostkeyalgorithms": server.HostKeyAlgorithms = value case "macs": server.MACs = value case "ciphers": server.Ciphers = value case "kexalgorithms": server.KexAlgorithms = value case "verifyhostkeydns": server.VerifyHostKeyDNS = value case "updatehostkeys": server.UpdateHostKeys = value case "hashknownhosts": server.HashKnownHosts = value case "visualhostkey": server.VisualHostKey = value default: return false } return true } // mapEnvironmentConfig maps environment and command execution fields func (r *Repository) mapEnvironmentConfig(server *domain.Server, key, value string) bool { switch key { case "localcommand": server.LocalCommand = value case "permitlocalcommand": server.PermitLocalCommand = value case "escapechar": server.EscapeChar = value case "sendenv": server.SendEnv = append(server.SendEnv, value) case "setenv": server.SetEnv = append(server.SetEnv, value) default: return false } return true } // mapDebugConfig maps debugging-related fields func (r *Repository) mapDebugConfig(server *domain.Server, key, value string) bool { switch key { case "loglevel": server.LogLevel = value default: return false } return true } // mergeMetadata merges additional metadata into the servers. func (r *Repository) mergeMetadata(servers []domain.Server, metadata map[string]ServerMetadata) []domain.Server { for i, server := range servers { servers[i].LastSeen = time.Time{} if meta, exists := metadata[server.Alias]; exists { servers[i].Tags = meta.Tags servers[i].SSHCount = meta.SSHCount if meta.LastSeen != "" { if lastSeen, err := time.Parse(time.RFC3339, meta.LastSeen); err == nil { servers[i].LastSeen = lastSeen } } if meta.PinnedAt != "" { if pinnedAt, err := time.Parse(time.RFC3339, meta.PinnedAt); err == nil { servers[i].PinnedAt = pinnedAt } } } } return servers } // convertConfigForwardToCLIFormat converts SSH config format forwarding spec to CLI format. // Config format: [bind_address:]port host:hostport // CLI format: [bind_address:]port:host:hostport func (r *Repository) convertConfigForwardToCLIFormat(forward string) string { // Find the last space which separates the local part from the remote part lastSpace := strings.LastIndex(forward, " ") if lastSpace != -1 { localPart := forward[:lastSpace] remotePart := forward[lastSpace+1:] // Join them with a colon for CLI format return localPart + ":" + remotePart } // If no space found, return as-is (might already be in CLI format) return forward }