|
|
|
// ssh.go
|
|
|
|
// Copyright (C) Andrew Woodlee 2023
|
|
|
|
// License: Apache-2.0
|
|
|
|
|
|
|
|
package backy
|
|
|
|
|
|
|
|
import (
|
|
|
|
"os"
|
|
|
|
"os/user"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/kevinburke/ssh_config"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"golang.org/x/crypto/ssh/knownhosts"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ConnectToSSHHost connects to a host by looking up the config values in the directory ~/.ssh/config
|
|
|
|
// Other than host, it does not yet respect other config values set in the backy config file.
|
|
|
|
// It returns an ssh.Client used to run commands against.
|
|
|
|
func (remoteConfig *Host) ConnectToSSHHost(log *zerolog.Logger) (*ssh.Client, error) {
|
|
|
|
|
|
|
|
var sshClient *ssh.Client
|
|
|
|
var connectErr error
|
|
|
|
|
|
|
|
khPath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")
|
|
|
|
f, _ := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "config"))
|
|
|
|
cfg, _ := ssh_config.Decode(f)
|
|
|
|
for _, host := range cfg.Hosts {
|
|
|
|
// var hostKey ssh.PublicKey
|
|
|
|
if host.Matches(remoteConfig.Host) {
|
|
|
|
var identityFile string
|
|
|
|
if remoteConfig.PrivateKeyPath == "" {
|
|
|
|
identityFile, _ = cfg.Get(remoteConfig.Host, "IdentityFile")
|
|
|
|
usr, _ := user.Current()
|
|
|
|
dir := usr.HomeDir
|
|
|
|
if identityFile == "~" {
|
|
|
|
// In case of "~", which won't be caught by the "else if"
|
|
|
|
identityFile = dir
|
|
|
|
} else if strings.HasPrefix(identityFile, "~/") {
|
|
|
|
// Use strings.HasPrefix so we don't match paths like
|
|
|
|
// "/something/~/something/"
|
|
|
|
identityFile = filepath.Join(dir, identityFile[2:])
|
|
|
|
}
|
|
|
|
remoteConfig.PrivateKeyPath = filepath.Join(identityFile)
|
|
|
|
log.Debug().Str("Private key path", remoteConfig.PrivateKeyPath).Send()
|
|
|
|
}
|
|
|
|
remoteConfig.HostName, _ = cfg.Get(remoteConfig.Host, "HostName")
|
|
|
|
remoteConfig.User, _ = cfg.Get(remoteConfig.Host, "User")
|
|
|
|
if remoteConfig.HostName == "" {
|
|
|
|
port, _ := cfg.Get(remoteConfig.Host, "Port")
|
|
|
|
if port == "" {
|
|
|
|
port = "22"
|
|
|
|
}
|
|
|
|
// remoteConfig.HostName[0] = remoteConfig.Host + ":" + port
|
|
|
|
} else {
|
|
|
|
// for index, hostName := range remoteConfig.HostName {
|
|
|
|
port, _ := cfg.Get(remoteConfig.Host, "Port")
|
|
|
|
if port == "" {
|
|
|
|
port = "22"
|
|
|
|
}
|
|
|
|
remoteConfig.HostName = remoteConfig.HostName + ":" + port
|
|
|
|
// remoteConfig.HostName[index] = hostName + ":" + port
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Add value/option to config for host key and add bool to check for host key
|
|
|
|
hostKeyCallback, err := knownhosts.New(khPath)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("could not create hostkeycallback function")
|
|
|
|
}
|
|
|
|
privateKey, err := os.ReadFile(remoteConfig.PrivateKeyPath)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("read private key error")
|
|
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey(privateKey)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("parse private key error")
|
|
|
|
}
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
|
|
User: remoteConfig.User,
|
|
|
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
|
|
|
|
HostKeyCallback: hostKeyCallback,
|
|
|
|
// HostKeyAlgorithms: []string{ssh.KeyAlgoECDSA256},
|
|
|
|
}
|
|
|
|
// for _, host := range remoteConfig.HostName {
|
|
|
|
log.Info().Msgf("Connecting to host %s", remoteConfig.HostName)
|
|
|
|
|
|
|
|
sshClient, connectErr = ssh.Dial("tcp", remoteConfig.HostName, sshConfig)
|
|
|
|
if connectErr != nil {
|
|
|
|
log.Fatal().Str("host", remoteConfig.HostName).Err(connectErr).Send()
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return sshClient, connectErr
|
|
|
|
}
|