feat: Add 60+ SSH config field support with enhanced UI (#50)

This commit is contained in:
Pei-Tang Huang
2025-09-18 20:36:08 +01:00
committed by GitHub
parent e75b61e819
commit 3b9f866d55
20 changed files with 6019 additions and 156 deletions
@@ -65,19 +65,230 @@ func (r *Repository) toDomainServer(cfg *ssh_config.Config) []domain.Server {
// 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) {
switch strings.ToLower(kvNode.Key) {
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 = kvNode.Value
server.Host = value
case "user":
server.User = kvNode.Value
server.User = value
case "port":
port, err := strconv.Atoi(kvNode.Value)
port, err := strconv.Atoi(value)
if err == nil {
server.Port = port
}
case "identityfile":
server.IdentityFiles = append(server.IdentityFiles, kvNode.Value)
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.
@@ -104,3 +315,19 @@ func (r *Repository) mergeMetadata(servers []domain.Server, metadata map[string]
}
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
}