Files
lazyssh/internal/adapters/data/ssh_config_file/mapper.go
T

334 lines
9.3 KiB
Go

// 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
}