435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
648 lines
15 KiB
Go
648 lines
15 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ssh
|
|
|
|
// Session implements an interactive session described in
|
|
// "RFC 4254, section 6".
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"sync"
|
|
)
|
|
|
|
type Signal string
|
|
|
|
// POSIX signals as listed in RFC 4254 Section 6.10.
|
|
const (
|
|
SIGABRT Signal = "ABRT"
|
|
SIGALRM Signal = "ALRM"
|
|
SIGFPE Signal = "FPE"
|
|
SIGHUP Signal = "HUP"
|
|
SIGILL Signal = "ILL"
|
|
SIGINT Signal = "INT"
|
|
SIGKILL Signal = "KILL"
|
|
SIGPIPE Signal = "PIPE"
|
|
SIGQUIT Signal = "QUIT"
|
|
SIGSEGV Signal = "SEGV"
|
|
SIGTERM Signal = "TERM"
|
|
SIGUSR1 Signal = "USR1"
|
|
SIGUSR2 Signal = "USR2"
|
|
)
|
|
|
|
var signals = map[Signal]int{
|
|
SIGABRT: 6,
|
|
SIGALRM: 14,
|
|
SIGFPE: 8,
|
|
SIGHUP: 1,
|
|
SIGILL: 4,
|
|
SIGINT: 2,
|
|
SIGKILL: 9,
|
|
SIGPIPE: 13,
|
|
SIGQUIT: 3,
|
|
SIGSEGV: 11,
|
|
SIGTERM: 15,
|
|
}
|
|
|
|
type TerminalModes map[uint8]uint32
|
|
|
|
// POSIX terminal mode flags as listed in RFC 4254 Section 8.
|
|
const (
|
|
tty_OP_END = 0
|
|
VINTR = 1
|
|
VQUIT = 2
|
|
VERASE = 3
|
|
VKILL = 4
|
|
VEOF = 5
|
|
VEOL = 6
|
|
VEOL2 = 7
|
|
VSTART = 8
|
|
VSTOP = 9
|
|
VSUSP = 10
|
|
VDSUSP = 11
|
|
VREPRINT = 12
|
|
VWERASE = 13
|
|
VLNEXT = 14
|
|
VFLUSH = 15
|
|
VSWTCH = 16
|
|
VSTATUS = 17
|
|
VDISCARD = 18
|
|
IGNPAR = 30
|
|
PARMRK = 31
|
|
INPCK = 32
|
|
ISTRIP = 33
|
|
INLCR = 34
|
|
IGNCR = 35
|
|
ICRNL = 36
|
|
IUCLC = 37
|
|
IXON = 38
|
|
IXANY = 39
|
|
IXOFF = 40
|
|
IMAXBEL = 41
|
|
ISIG = 50
|
|
ICANON = 51
|
|
XCASE = 52
|
|
ECHO = 53
|
|
ECHOE = 54
|
|
ECHOK = 55
|
|
ECHONL = 56
|
|
NOFLSH = 57
|
|
TOSTOP = 58
|
|
IEXTEN = 59
|
|
ECHOCTL = 60
|
|
ECHOKE = 61
|
|
PENDIN = 62
|
|
OPOST = 70
|
|
OLCUC = 71
|
|
ONLCR = 72
|
|
OCRNL = 73
|
|
ONOCR = 74
|
|
ONLRET = 75
|
|
CS7 = 90
|
|
CS8 = 91
|
|
PARENB = 92
|
|
PARODD = 93
|
|
TTY_OP_ISPEED = 128
|
|
TTY_OP_OSPEED = 129
|
|
)
|
|
|
|
// A Session represents a connection to a remote command or shell.
|
|
type Session struct {
|
|
// Stdin specifies the remote process's standard input.
|
|
// If Stdin is nil, the remote process reads from an empty
|
|
// bytes.Buffer.
|
|
Stdin io.Reader
|
|
|
|
// Stdout and Stderr specify the remote process's standard
|
|
// output and error.
|
|
//
|
|
// If either is nil, Run connects the corresponding file
|
|
// descriptor to an instance of ioutil.Discard. There is a
|
|
// fixed amount of buffering that is shared for the two streams.
|
|
// If either blocks it may eventually cause the remote
|
|
// command to block.
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
|
|
ch Channel // the channel backing this session
|
|
started bool // true once Start, Run or Shell is invoked.
|
|
copyFuncs []func() error
|
|
errors chan error // one send per copyFunc
|
|
|
|
// true if pipe method is active
|
|
stdinpipe, stdoutpipe, stderrpipe bool
|
|
|
|
// stdinPipeWriter is non-nil if StdinPipe has not been called
|
|
// and Stdin was specified by the user; it is the write end of
|
|
// a pipe connecting Session.Stdin to the stdin channel.
|
|
stdinPipeWriter io.WriteCloser
|
|
|
|
exitStatus chan error
|
|
}
|
|
|
|
// SendRequest sends an out-of-band channel request on the SSH channel
|
|
// underlying the session.
|
|
func (s *Session) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
|
|
return s.ch.SendRequest(name, wantReply, payload)
|
|
}
|
|
|
|
func (s *Session) Close() error {
|
|
return s.ch.Close()
|
|
}
|
|
|
|
// RFC 4254 Section 6.4.
|
|
type setenvRequest struct {
|
|
Name string
|
|
Value string
|
|
}
|
|
|
|
// Setenv sets an environment variable that will be applied to any
|
|
// command executed by Shell or Run.
|
|
func (s *Session) Setenv(name, value string) error {
|
|
msg := setenvRequest{
|
|
Name: name,
|
|
Value: value,
|
|
}
|
|
ok, err := s.ch.SendRequest("env", true, Marshal(&msg))
|
|
if err == nil && !ok {
|
|
err = errors.New("ssh: setenv failed")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RFC 4254 Section 6.2.
|
|
type ptyRequestMsg struct {
|
|
Term string
|
|
Columns uint32
|
|
Rows uint32
|
|
Width uint32
|
|
Height uint32
|
|
Modelist string
|
|
}
|
|
|
|
// RequestPty requests the association of a pty with the session on the remote host.
|
|
func (s *Session) RequestPty(term string, h, w int, termmodes TerminalModes) error {
|
|
var tm []byte
|
|
for k, v := range termmodes {
|
|
kv := struct {
|
|
Key byte
|
|
Val uint32
|
|
}{k, v}
|
|
|
|
tm = append(tm, Marshal(&kv)...)
|
|
}
|
|
tm = append(tm, tty_OP_END)
|
|
req := ptyRequestMsg{
|
|
Term: term,
|
|
Columns: uint32(w),
|
|
Rows: uint32(h),
|
|
Width: uint32(w * 8),
|
|
Height: uint32(h * 8),
|
|
Modelist: string(tm),
|
|
}
|
|
ok, err := s.ch.SendRequest("pty-req", true, Marshal(&req))
|
|
if err == nil && !ok {
|
|
err = errors.New("ssh: pty-req failed")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RFC 4254 Section 6.5.
|
|
type subsystemRequestMsg struct {
|
|
Subsystem string
|
|
}
|
|
|
|
// RequestSubsystem requests the association of a subsystem with the session on the remote host.
|
|
// A subsystem is a predefined command that runs in the background when the ssh session is initiated
|
|
func (s *Session) RequestSubsystem(subsystem string) error {
|
|
msg := subsystemRequestMsg{
|
|
Subsystem: subsystem,
|
|
}
|
|
ok, err := s.ch.SendRequest("subsystem", true, Marshal(&msg))
|
|
if err == nil && !ok {
|
|
err = errors.New("ssh: subsystem request failed")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RFC 4254 Section 6.7.
|
|
type ptyWindowChangeMsg struct {
|
|
Columns uint32
|
|
Rows uint32
|
|
Width uint32
|
|
Height uint32
|
|
}
|
|
|
|
// WindowChange informs the remote host about a terminal window dimension change to h rows and w columns.
|
|
func (s *Session) WindowChange(h, w int) error {
|
|
req := ptyWindowChangeMsg{
|
|
Columns: uint32(w),
|
|
Rows: uint32(h),
|
|
Width: uint32(w * 8),
|
|
Height: uint32(h * 8),
|
|
}
|
|
_, err := s.ch.SendRequest("window-change", false, Marshal(&req))
|
|
return err
|
|
}
|
|
|
|
// RFC 4254 Section 6.9.
|
|
type signalMsg struct {
|
|
Signal string
|
|
}
|
|
|
|
// Signal sends the given signal to the remote process.
|
|
// sig is one of the SIG* constants.
|
|
func (s *Session) Signal(sig Signal) error {
|
|
msg := signalMsg{
|
|
Signal: string(sig),
|
|
}
|
|
|
|
_, err := s.ch.SendRequest("signal", false, Marshal(&msg))
|
|
return err
|
|
}
|
|
|
|
// RFC 4254 Section 6.5.
|
|
type execMsg struct {
|
|
Command string
|
|
}
|
|
|
|
// Start runs cmd on the remote host. Typically, the remote
|
|
// server passes cmd to the shell for interpretation.
|
|
// A Session only accepts one call to Run, Start or Shell.
|
|
func (s *Session) Start(cmd string) error {
|
|
if s.started {
|
|
return errors.New("ssh: session already started")
|
|
}
|
|
req := execMsg{
|
|
Command: cmd,
|
|
}
|
|
|
|
ok, err := s.ch.SendRequest("exec", true, Marshal(&req))
|
|
if err == nil && !ok {
|
|
err = fmt.Errorf("ssh: command %v failed", cmd)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.start()
|
|
}
|
|
|
|
// Run runs cmd on the remote host. Typically, the remote
|
|
// server passes cmd to the shell for interpretation.
|
|
// A Session only accepts one call to Run, Start, Shell, Output,
|
|
// or CombinedOutput.
|
|
//
|
|
// The returned error is nil if the command runs, has no problems
|
|
// copying stdin, stdout, and stderr, and exits with a zero exit
|
|
// status.
|
|
//
|
|
// If the remote server does not send an exit status, an error of type
|
|
// *ExitMissingError is returned. If the command completes
|
|
// unsuccessfully or is interrupted by a signal, the error is of type
|
|
// *ExitError. Other error types may be returned for I/O problems.
|
|
func (s *Session) Run(cmd string) error {
|
|
err := s.Start(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.Wait()
|
|
}
|
|
|
|
// Output runs cmd on the remote host and returns its standard output.
|
|
func (s *Session) Output(cmd string) ([]byte, error) {
|
|
if s.Stdout != nil {
|
|
return nil, errors.New("ssh: Stdout already set")
|
|
}
|
|
var b bytes.Buffer
|
|
s.Stdout = &b
|
|
err := s.Run(cmd)
|
|
return b.Bytes(), err
|
|
}
|
|
|
|
type singleWriter struct {
|
|
b bytes.Buffer
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (w *singleWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.b.Write(p)
|
|
}
|
|
|
|
// CombinedOutput runs cmd on the remote host and returns its combined
|
|
// standard output and standard error.
|
|
func (s *Session) CombinedOutput(cmd string) ([]byte, error) {
|
|
if s.Stdout != nil {
|
|
return nil, errors.New("ssh: Stdout already set")
|
|
}
|
|
if s.Stderr != nil {
|
|
return nil, errors.New("ssh: Stderr already set")
|
|
}
|
|
var b singleWriter
|
|
s.Stdout = &b
|
|
s.Stderr = &b
|
|
err := s.Run(cmd)
|
|
return b.b.Bytes(), err
|
|
}
|
|
|
|
// Shell starts a login shell on the remote host. A Session only
|
|
// accepts one call to Run, Start, Shell, Output, or CombinedOutput.
|
|
func (s *Session) Shell() error {
|
|
if s.started {
|
|
return errors.New("ssh: session already started")
|
|
}
|
|
|
|
ok, err := s.ch.SendRequest("shell", true, nil)
|
|
if err == nil && !ok {
|
|
return errors.New("ssh: could not start shell")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.start()
|
|
}
|
|
|
|
func (s *Session) start() error {
|
|
s.started = true
|
|
|
|
type F func(*Session)
|
|
for _, setupFd := range []F{(*Session).stdin, (*Session).stdout, (*Session).stderr} {
|
|
setupFd(s)
|
|
}
|
|
|
|
s.errors = make(chan error, len(s.copyFuncs))
|
|
for _, fn := range s.copyFuncs {
|
|
go func(fn func() error) {
|
|
s.errors <- fn()
|
|
}(fn)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Wait waits for the remote command to exit.
|
|
//
|
|
// The returned error is nil if the command runs, has no problems
|
|
// copying stdin, stdout, and stderr, and exits with a zero exit
|
|
// status.
|
|
//
|
|
// If the remote server does not send an exit status, an error of type
|
|
// *ExitMissingError is returned. If the command completes
|
|
// unsuccessfully or is interrupted by a signal, the error is of type
|
|
// *ExitError. Other error types may be returned for I/O problems.
|
|
func (s *Session) Wait() error {
|
|
if !s.started {
|
|
return errors.New("ssh: session not started")
|
|
}
|
|
waitErr := <-s.exitStatus
|
|
|
|
if s.stdinPipeWriter != nil {
|
|
s.stdinPipeWriter.Close()
|
|
}
|
|
var copyError error
|
|
for range s.copyFuncs {
|
|
if err := <-s.errors; err != nil && copyError == nil {
|
|
copyError = err
|
|
}
|
|
}
|
|
if waitErr != nil {
|
|
return waitErr
|
|
}
|
|
return copyError
|
|
}
|
|
|
|
func (s *Session) wait(reqs <-chan *Request) error {
|
|
wm := Waitmsg{status: -1}
|
|
// Wait for msg channel to be closed before returning.
|
|
for msg := range reqs {
|
|
switch msg.Type {
|
|
case "exit-status":
|
|
wm.status = int(binary.BigEndian.Uint32(msg.Payload))
|
|
case "exit-signal":
|
|
var sigval struct {
|
|
Signal string
|
|
CoreDumped bool
|
|
Error string
|
|
Lang string
|
|
}
|
|
if err := Unmarshal(msg.Payload, &sigval); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Must sanitize strings?
|
|
wm.signal = sigval.Signal
|
|
wm.msg = sigval.Error
|
|
wm.lang = sigval.Lang
|
|
default:
|
|
// This handles keepalives and matches
|
|
// OpenSSH's behaviour.
|
|
if msg.WantReply {
|
|
msg.Reply(false, nil)
|
|
}
|
|
}
|
|
}
|
|
if wm.status == 0 {
|
|
return nil
|
|
}
|
|
if wm.status == -1 {
|
|
// exit-status was never sent from server
|
|
if wm.signal == "" {
|
|
// signal was not sent either. RFC 4254
|
|
// section 6.10 recommends against this
|
|
// behavior, but it is allowed, so we let
|
|
// clients handle it.
|
|
return &ExitMissingError{}
|
|
}
|
|
wm.status = 128
|
|
if _, ok := signals[Signal(wm.signal)]; ok {
|
|
wm.status += signals[Signal(wm.signal)]
|
|
}
|
|
}
|
|
|
|
return &ExitError{wm}
|
|
}
|
|
|
|
// ExitMissingError is returned if a session is torn down cleanly, but
|
|
// the server sends no confirmation of the exit status.
|
|
type ExitMissingError struct{}
|
|
|
|
func (e *ExitMissingError) Error() string {
|
|
return "wait: remote command exited without exit status or exit signal"
|
|
}
|
|
|
|
func (s *Session) stdin() {
|
|
if s.stdinpipe {
|
|
return
|
|
}
|
|
var stdin io.Reader
|
|
if s.Stdin == nil {
|
|
stdin = new(bytes.Buffer)
|
|
} else {
|
|
r, w := io.Pipe()
|
|
go func() {
|
|
_, err := io.Copy(w, s.Stdin)
|
|
w.CloseWithError(err)
|
|
}()
|
|
stdin, s.stdinPipeWriter = r, w
|
|
}
|
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
|
_, err := io.Copy(s.ch, stdin)
|
|
if err1 := s.ch.CloseWrite(); err == nil && err1 != io.EOF {
|
|
err = err1
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (s *Session) stdout() {
|
|
if s.stdoutpipe {
|
|
return
|
|
}
|
|
if s.Stdout == nil {
|
|
s.Stdout = ioutil.Discard
|
|
}
|
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
|
_, err := io.Copy(s.Stdout, s.ch)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (s *Session) stderr() {
|
|
if s.stderrpipe {
|
|
return
|
|
}
|
|
if s.Stderr == nil {
|
|
s.Stderr = ioutil.Discard
|
|
}
|
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
|
_, err := io.Copy(s.Stderr, s.ch.Stderr())
|
|
return err
|
|
})
|
|
}
|
|
|
|
// sessionStdin reroutes Close to CloseWrite.
|
|
type sessionStdin struct {
|
|
io.Writer
|
|
ch Channel
|
|
}
|
|
|
|
func (s *sessionStdin) Close() error {
|
|
return s.ch.CloseWrite()
|
|
}
|
|
|
|
// StdinPipe returns a pipe that will be connected to the
|
|
// remote command's standard input when the command starts.
|
|
func (s *Session) StdinPipe() (io.WriteCloser, error) {
|
|
if s.Stdin != nil {
|
|
return nil, errors.New("ssh: Stdin already set")
|
|
}
|
|
if s.started {
|
|
return nil, errors.New("ssh: StdinPipe after process started")
|
|
}
|
|
s.stdinpipe = true
|
|
return &sessionStdin{s.ch, s.ch}, nil
|
|
}
|
|
|
|
// StdoutPipe returns a pipe that will be connected to the
|
|
// remote command's standard output when the command starts.
|
|
// There is a fixed amount of buffering that is shared between
|
|
// stdout and stderr streams. If the StdoutPipe reader is
|
|
// not serviced fast enough it may eventually cause the
|
|
// remote command to block.
|
|
func (s *Session) StdoutPipe() (io.Reader, error) {
|
|
if s.Stdout != nil {
|
|
return nil, errors.New("ssh: Stdout already set")
|
|
}
|
|
if s.started {
|
|
return nil, errors.New("ssh: StdoutPipe after process started")
|
|
}
|
|
s.stdoutpipe = true
|
|
return s.ch, nil
|
|
}
|
|
|
|
// StderrPipe returns a pipe that will be connected to the
|
|
// remote command's standard error when the command starts.
|
|
// There is a fixed amount of buffering that is shared between
|
|
// stdout and stderr streams. If the StderrPipe reader is
|
|
// not serviced fast enough it may eventually cause the
|
|
// remote command to block.
|
|
func (s *Session) StderrPipe() (io.Reader, error) {
|
|
if s.Stderr != nil {
|
|
return nil, errors.New("ssh: Stderr already set")
|
|
}
|
|
if s.started {
|
|
return nil, errors.New("ssh: StderrPipe after process started")
|
|
}
|
|
s.stderrpipe = true
|
|
return s.ch.Stderr(), nil
|
|
}
|
|
|
|
// newSession returns a new interactive session on the remote host.
|
|
func newSession(ch Channel, reqs <-chan *Request) (*Session, error) {
|
|
s := &Session{
|
|
ch: ch,
|
|
}
|
|
s.exitStatus = make(chan error, 1)
|
|
go func() {
|
|
s.exitStatus <- s.wait(reqs)
|
|
}()
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// An ExitError reports unsuccessful completion of a remote command.
|
|
type ExitError struct {
|
|
Waitmsg
|
|
}
|
|
|
|
func (e *ExitError) Error() string {
|
|
return e.Waitmsg.String()
|
|
}
|
|
|
|
// Waitmsg stores the information about an exited remote command
|
|
// as reported by Wait.
|
|
type Waitmsg struct {
|
|
status int
|
|
signal string
|
|
msg string
|
|
lang string
|
|
}
|
|
|
|
// ExitStatus returns the exit status of the remote command.
|
|
func (w Waitmsg) ExitStatus() int {
|
|
return w.status
|
|
}
|
|
|
|
// Signal returns the exit signal of the remote command if
|
|
// it was terminated violently.
|
|
func (w Waitmsg) Signal() string {
|
|
return w.signal
|
|
}
|
|
|
|
// Msg returns the exit message given by the remote command
|
|
func (w Waitmsg) Msg() string {
|
|
return w.msg
|
|
}
|
|
|
|
// Lang returns the language tag. See RFC 3066
|
|
func (w Waitmsg) Lang() string {
|
|
return w.lang
|
|
}
|
|
|
|
func (w Waitmsg) String() string {
|
|
str := fmt.Sprintf("Process exited with status %v", w.status)
|
|
if w.signal != "" {
|
|
str += fmt.Sprintf(" from signal %v", w.signal)
|
|
}
|
|
if w.msg != "" {
|
|
str += fmt.Sprintf(". Reason was: %v", w.msg)
|
|
}
|
|
return str
|
|
}
|