0
0
forked from toolshed/abra
Files
.gitea
cli
cmd
pkg
app
catalogue
client
compose
config
dns
git
recipe
secret
server
ssh
ssh.go
upstream
web
scripts
tests
.drone.yml
.envrc.sample
.gitignore
.goreleaser.yml
Makefile
README.md
go.mod
go.sum
abra/pkg/ssh/ssh.go

227 lines
4.6 KiB
Go

package ssh
import (
"bufio"
"bytes"
"fmt"
"io"
"os/user"
"sync"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/kevinburke/ssh_config"
"github.com/sfreiberg/simplessh"
"github.com/sirupsen/logrus"
)
// HostConfig is a SSH host config.
type HostConfig struct {
Host string
IdentityFile string
Port string
User string
}
// GetHostConfig retrieves a ~/.ssh/config config for a host.
func GetHostConfig(hostname, username, port string) (HostConfig, error) {
var hostConfig HostConfig
var host, idf string
if host = ssh_config.Get(hostname, "Hostname"); host == "" {
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname)
host = hostname
}
if username == "" {
if username = ssh_config.Get(hostname, "User"); username == "" {
systemUser, err := user.Current()
if err != nil {
return hostConfig, err
}
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
username = systemUser.Username
}
}
if port == "" {
if port = ssh_config.Get(hostname, "Port"); port == "" {
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22")
port = "22"
}
}
idf = ssh_config.Get(hostname, "IdentityFile")
hostConfig.Host = host
if idf != "" {
hostConfig.IdentityFile = idf
}
hostConfig.Port = port
hostConfig.User = username
logrus.Debugf("constructed SSH config %s for %s", hostConfig, hostname)
return hostConfig, nil
}
// New creates a new SSH client connection.
func New(domainName, sshAuth, username, port string) (*simplessh.Client, error) {
var client *simplessh.Client
hostConfig, err := GetHostConfig(domainName, username, port)
if err != nil {
return client, err
}
if sshAuth == "identity-file" {
var err error
client, err = simplessh.ConnectWithAgentTimeout(hostConfig.Host, hostConfig.User, 5*time.Second)
if err != nil {
return client, err
}
} else {
password := ""
prompt := &survey.Password{
Message: "SSH password?",
}
if err := survey.AskOne(prompt, &password); err != nil {
return client, err
}
var err error
client, err = simplessh.ConnectWithPasswordTimeout(hostConfig.Host, hostConfig.User, password, 5*time.Second)
if err != nil {
return client, err
}
}
return client, nil
}
// sudoWriter supports sudo command handling.
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
type sudoWriter struct {
b bytes.Buffer
pw string
stdin io.Writer
m sync.Mutex
}
// Write satisfies the write interface for sudoWriter.
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
func (w *sudoWriter) Write(p []byte) (int, error) {
if string(p) == "sudo_password" {
w.stdin.Write([]byte(w.pw + "\n"))
w.pw = ""
return len(p), nil
}
w.m.Lock()
defer w.m.Unlock()
return w.b.Write(p)
}
// RunSudoCmd runs SSH commands and streams output.
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
func RunSudoCmd(cmd, passwd string, cl *simplessh.Client) error {
session, err := cl.SSHClient.NewSession()
if err != nil {
return err
}
defer session.Close()
cmd = "sudo -p " + "sudo_password" + " -S " + cmd
w := &sudoWriter{
pw: passwd,
}
w.stdin, err = session.StdinPipe()
if err != nil {
return err
}
session.Stdout = w
session.Stderr = w
done := make(chan struct{})
scanner := bufio.NewScanner(session.Stdin)
go func() {
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
done <- struct{}{}
}()
if err := session.Start(cmd); err != nil {
return err
}
<-done
if err := session.Wait(); err != nil {
return err
}
return err
}
// Exec runs a command on a remote and streams output.
// https://github.com/sfreiberg/simplessh/blob/master/simplessh.go
func Exec(cmd string, cl *simplessh.Client) error {
session, err := cl.SSHClient.NewSession()
if err != nil {
return err
}
defer session.Close()
stdout, err := session.StdoutPipe()
if err != nil {
return err
}
stderr, err := session.StdoutPipe()
if err != nil {
return err
}
stdoutDone := make(chan struct{})
stdoutScanner := bufio.NewScanner(stdout)
go func() {
for stdoutScanner.Scan() {
line := stdoutScanner.Text()
fmt.Println(line)
}
stdoutDone <- struct{}{}
}()
stderrDone := make(chan struct{})
stderrScanner := bufio.NewScanner(stderr)
go func() {
for stderrScanner.Scan() {
line := stderrScanner.Text()
fmt.Println(line)
}
stderrDone <- struct{}{}
}()
if err := session.Start(cmd); err != nil {
return err
}
<-stdoutDone
<-stderrDone
if err := session.Wait(); err != nil {
return err
}
return nil
}