|
|
// 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 }
|