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