mirror of
https://github.com/Adembc/lazyssh.git
synced 2026-07-14 12:13:34 +02:00
334 lines
9.3 KiB
Go
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
|
|
}
|