mirror of https://github.com/docker/docs.git
314 lines
7.0 KiB
Go
314 lines
7.0 KiB
Go
package ssh
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/pkg/term"
|
|
"github.com/docker/machine/libmachine/log"
|
|
"github.com/docker/machine/libmachine/mcnutils"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
type Client interface {
|
|
Output(command string) (string, error)
|
|
Shell(args ...string) error
|
|
}
|
|
|
|
type ExternalClient struct {
|
|
BaseArgs []string
|
|
BinaryPath string
|
|
}
|
|
|
|
type NativeClient struct {
|
|
Config ssh.ClientConfig
|
|
Hostname string
|
|
Port int
|
|
}
|
|
|
|
type Auth struct {
|
|
Passwords []string
|
|
Keys []string
|
|
}
|
|
|
|
type ClientType string
|
|
|
|
const (
|
|
maxDialAttempts = 10
|
|
)
|
|
|
|
const (
|
|
External ClientType = "external"
|
|
Native ClientType = "native"
|
|
)
|
|
|
|
var (
|
|
baseSSHArgs = []string{
|
|
"-o", "PasswordAuthentication=no",
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "LogLevel=quiet", // suppress "Warning: Permanently added '[localhost]:2022' (ECDSA) to the list of known hosts."
|
|
"-o", "ConnectionAttempts=3", // retry 3 times if SSH connection fails
|
|
"-o", "ConnectTimeout=10", // timeout after 10 seconds
|
|
"-o", "ControlMaster=no", // disable ssh multiplexing
|
|
"-o", "ControlPath=no",
|
|
}
|
|
defaultClientType = External
|
|
)
|
|
|
|
func SetDefaultClient(clientType ClientType) {
|
|
// Allow over-riding of default client type, so that even if ssh binary
|
|
// is found in PATH we can still use the Go native implementation if
|
|
// desired.
|
|
switch clientType {
|
|
case External:
|
|
defaultClientType = External
|
|
case Native:
|
|
defaultClientType = Native
|
|
}
|
|
}
|
|
|
|
func NewClient(user string, host string, port int, auth *Auth) (Client, error) {
|
|
sshBinaryPath, err := exec.LookPath("ssh")
|
|
if err != nil {
|
|
log.Debug("SSH binary not found, using native Go implementation")
|
|
client, err := NewNativeClient(user, host, port, auth)
|
|
log.Debug(client)
|
|
return client, err
|
|
}
|
|
|
|
if defaultClientType == Native {
|
|
log.Debug("Using SSH client type: native")
|
|
client, err := NewNativeClient(user, host, port, auth)
|
|
log.Debug(client)
|
|
return client, err
|
|
}
|
|
|
|
log.Debug("Using SSH client type: external")
|
|
client, err := NewExternalClient(sshBinaryPath, user, host, port, auth)
|
|
log.Debug(client)
|
|
return client, err
|
|
}
|
|
|
|
func NewNativeClient(user, host string, port int, auth *Auth) (Client, error) {
|
|
config, err := NewNativeConfig(user, auth)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error getting config for native Go SSH: %s", err)
|
|
}
|
|
|
|
return NativeClient{
|
|
Config: config,
|
|
Hostname: host,
|
|
Port: port,
|
|
}, nil
|
|
}
|
|
|
|
func NewNativeConfig(user string, auth *Auth) (ssh.ClientConfig, error) {
|
|
var (
|
|
authMethods []ssh.AuthMethod
|
|
)
|
|
|
|
for _, k := range auth.Keys {
|
|
key, err := ioutil.ReadFile(k)
|
|
if err != nil {
|
|
return ssh.ClientConfig{}, err
|
|
}
|
|
|
|
privateKey, err := ssh.ParsePrivateKey(key)
|
|
if err != nil {
|
|
return ssh.ClientConfig{}, err
|
|
}
|
|
|
|
authMethods = append(authMethods, ssh.PublicKeys(privateKey))
|
|
}
|
|
|
|
for _, p := range auth.Passwords {
|
|
authMethods = append(authMethods, ssh.Password(p))
|
|
}
|
|
|
|
return ssh.ClientConfig{
|
|
User: user,
|
|
Auth: authMethods,
|
|
}, nil
|
|
}
|
|
|
|
func (client NativeClient) dialSuccess() bool {
|
|
if _, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config); err != nil {
|
|
log.Debugf("Error dialing TCP: %s", err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (client NativeClient) session(command string) (*ssh.Session, error) {
|
|
if err := mcnutils.WaitFor(client.dialSuccess); err != nil {
|
|
return nil, fmt.Errorf("Error attempting SSH client dial: %s", err)
|
|
}
|
|
|
|
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Mysterious error dialing TCP for SSH (we already succeeded at least once) : %s", err)
|
|
}
|
|
|
|
return conn.NewSession()
|
|
}
|
|
|
|
func (client NativeClient) Output(command string) (string, error) {
|
|
session, err := client.session(command)
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
|
|
output, err := session.CombinedOutput(command)
|
|
defer session.Close()
|
|
|
|
return string(output), err
|
|
}
|
|
|
|
func (client NativeClient) OutputWithPty(command string) (string, error) {
|
|
session, err := client.session(command)
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
|
|
fd := int(os.Stdin.Fd())
|
|
|
|
termWidth, termHeight, err := terminal.GetSize(fd)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
modes := ssh.TerminalModes{
|
|
ssh.ECHO: 0,
|
|
ssh.TTY_OP_ISPEED: 14400,
|
|
ssh.TTY_OP_OSPEED: 14400,
|
|
}
|
|
|
|
// request tty -- fixes error with hosts that use
|
|
// "Defaults requiretty" in /etc/sudoers - I'm looking at you RedHat
|
|
if err := session.RequestPty("xterm", termHeight, termWidth, modes); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
output, err := session.CombinedOutput(command)
|
|
defer session.Close()
|
|
|
|
return string(output), err
|
|
}
|
|
|
|
func (client NativeClient) Shell(args ...string) error {
|
|
var (
|
|
termWidth, termHeight int
|
|
)
|
|
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", client.Hostname, client.Port), &client.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer session.Close()
|
|
|
|
session.Stdout = os.Stdout
|
|
session.Stderr = os.Stderr
|
|
session.Stdin = os.Stdin
|
|
|
|
modes := ssh.TerminalModes{
|
|
ssh.ECHO: 1,
|
|
}
|
|
|
|
fd := os.Stdin.Fd()
|
|
|
|
if term.IsTerminal(fd) {
|
|
oldState, err := term.MakeRaw(fd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer term.RestoreTerminal(fd, oldState)
|
|
|
|
winsize, err := term.GetWinsize(fd)
|
|
if err != nil {
|
|
termWidth = 80
|
|
termHeight = 24
|
|
} else {
|
|
termWidth = int(winsize.Width)
|
|
termHeight = int(winsize.Height)
|
|
}
|
|
}
|
|
|
|
if err := session.RequestPty("xterm", termHeight, termWidth, modes); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
if err := session.Shell(); err != nil {
|
|
return err
|
|
}
|
|
session.Wait()
|
|
} else {
|
|
session.Run(strings.Join(args, " "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewExternalClient(sshBinaryPath, user, host string, port int, auth *Auth) (ExternalClient, error) {
|
|
client := ExternalClient{
|
|
BinaryPath: sshBinaryPath,
|
|
}
|
|
|
|
args := append(baseSSHArgs, fmt.Sprintf("%s@%s", user, host))
|
|
|
|
// If no identities are explicitly provided, also look at the identities
|
|
// offered by ssh-agent
|
|
if len(auth.Keys) > 0 {
|
|
args = append(args, "-o", "IdentitiesOnly=yes")
|
|
}
|
|
|
|
// Specify which private keys to use to authorize the SSH request.
|
|
for _, privateKeyPath := range auth.Keys {
|
|
if privateKeyPath != "" {
|
|
args = append(args, "-i", privateKeyPath)
|
|
}
|
|
}
|
|
|
|
// Set which port to use for SSH.
|
|
args = append(args, "-p", fmt.Sprintf("%d", port))
|
|
|
|
client.BaseArgs = args
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func getSSHCmd(binaryPath string, args ...string) *exec.Cmd {
|
|
return exec.Command(binaryPath, args...)
|
|
}
|
|
|
|
func (client ExternalClient) Output(command string) (string, error) {
|
|
args := append(client.BaseArgs, command)
|
|
cmd := getSSHCmd(client.BinaryPath, args...)
|
|
output, err := cmd.CombinedOutput()
|
|
return string(output), err
|
|
}
|
|
|
|
func (client ExternalClient) Shell(args ...string) error {
|
|
args = append(client.BaseArgs, args...)
|
|
cmd := getSSHCmd(client.BinaryPath, args...)
|
|
|
|
log.Debug(cmd)
|
|
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
return cmd.Run()
|
|
}
|