| // 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 |
| IUTF8 = 42 // RFC 8160 |
| 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 |
| } |