forked from toolshed/abra
.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
227 lines
4.6 KiB
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
|
|
}
|