These are the cosmetic cleanups from the bits of code that I rereviewed. 1) stringLength now takes a int; the length of the string. Too many callers were allocating with stringLength([]byte(s)) and stringLength only needs to call len(). 2) agent.go now has sendAndReceive to remove logic that was duplicated. 3) We now reject negative DH values 4) We now reject empty packets rather than crashing. R=dave, jonathan.mark.pittman CC=golang-dev https://golang.org/cl/6061052
495 lines
12 KiB
Go
495 lines
12 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"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
)
|
|
|
|
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,
|
|
}
|
|
|
|
// 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
|
|
|
|
*clientChan // 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
|
|
}
|
|
|
|
// RFC 4254 Section 6.4.
|
|
type setenvRequest struct {
|
|
PeersId uint32
|
|
Request string
|
|
WantReply bool
|
|
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 {
|
|
req := setenvRequest{
|
|
PeersId: s.peersId,
|
|
Request: "env",
|
|
WantReply: true,
|
|
Name: name,
|
|
Value: value,
|
|
}
|
|
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
|
return err
|
|
}
|
|
return s.waitForResponse()
|
|
}
|
|
|
|
// An empty mode list, see RFC 4254 Section 8.
|
|
var emptyModelist = "\x00"
|
|
|
|
// RFC 4254 Section 6.2.
|
|
type ptyRequestMsg struct {
|
|
PeersId uint32
|
|
Request string
|
|
WantReply bool
|
|
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) error {
|
|
req := ptyRequestMsg{
|
|
PeersId: s.peersId,
|
|
Request: "pty-req",
|
|
WantReply: true,
|
|
Term: term,
|
|
Columns: uint32(w),
|
|
Rows: uint32(h),
|
|
Width: uint32(w * 8),
|
|
Height: uint32(h * 8),
|
|
Modelist: emptyModelist,
|
|
}
|
|
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
|
return err
|
|
}
|
|
return s.waitForResponse()
|
|
}
|
|
|
|
// RFC 4254 Section 6.9.
|
|
type signalMsg struct {
|
|
PeersId uint32
|
|
Request string
|
|
WantReply bool
|
|
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 {
|
|
req := signalMsg{
|
|
PeersId: s.peersId,
|
|
Request: "signal",
|
|
WantReply: false,
|
|
Signal: string(sig),
|
|
}
|
|
return s.writePacket(marshal(msgChannelRequest, req))
|
|
}
|
|
|
|
// RFC 4254 Section 6.5.
|
|
type execMsg struct {
|
|
PeersId uint32
|
|
Request string
|
|
WantReply bool
|
|
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{
|
|
PeersId: s.peersId,
|
|
Request: "exec",
|
|
WantReply: true,
|
|
Command: cmd,
|
|
}
|
|
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.waitForResponse(); err != nil {
|
|
return fmt.Errorf("ssh: could not execute command %s: %v", cmd, 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 or Shell.
|
|
//
|
|
// 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 command fails to run or doesn't complete successfully, 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()
|
|
}
|
|
|
|
// Shell starts a login shell on the remote host. A Session only
|
|
// accepts one call to Run, Start or Shell.
|
|
func (s *Session) Shell() error {
|
|
if s.started {
|
|
return errors.New("ssh: session already started")
|
|
}
|
|
req := channelRequestMsg{
|
|
PeersId: s.peersId,
|
|
Request: "shell",
|
|
WantReply: true,
|
|
}
|
|
if err := s.writePacket(marshal(msgChannelRequest, req)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.waitForResponse(); err != nil {
|
|
return fmt.Errorf("ssh: cound not execute shell: %v", err)
|
|
}
|
|
return s.start()
|
|
}
|
|
|
|
func (s *Session) waitForResponse() error {
|
|
msg := <-s.msg
|
|
switch msg.(type) {
|
|
case *channelRequestSuccessMsg:
|
|
return nil
|
|
case *channelRequestFailureMsg:
|
|
return errors.New("ssh: request failed")
|
|
}
|
|
return fmt.Errorf("ssh: unknown packet %T received: %v", msg, msg)
|
|
}
|
|
|
|
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 command fails to run or doesn't complete successfully, 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.wait()
|
|
|
|
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() error {
|
|
wm := Waitmsg{status: -1}
|
|
|
|
// Wait for msg channel to be closed before returning.
|
|
for msg := range s.msg {
|
|
switch msg := msg.(type) {
|
|
case *channelRequestMsg:
|
|
switch msg.Request {
|
|
case "exit-status":
|
|
d := msg.RequestSpecificData
|
|
wm.status = int(d[0])<<24 | int(d[1])<<16 | int(d[2])<<8 | int(d[3])
|
|
case "exit-signal":
|
|
signal, rest, ok := parseString(msg.RequestSpecificData)
|
|
if !ok {
|
|
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
|
}
|
|
wm.signal = safeString(string(signal))
|
|
|
|
// skip coreDumped bool
|
|
if len(rest) == 0 {
|
|
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
|
}
|
|
rest = rest[1:]
|
|
|
|
errmsg, rest, ok := parseString(rest)
|
|
if !ok {
|
|
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
|
}
|
|
wm.msg = safeString(string(errmsg))
|
|
|
|
lang, _, ok := parseString(rest)
|
|
if !ok {
|
|
return fmt.Errorf("wait: could not parse request data: %v", msg.RequestSpecificData)
|
|
}
|
|
wm.lang = safeString(string(lang))
|
|
default:
|
|
return fmt.Errorf("wait: unexpected channel request: %v", msg)
|
|
}
|
|
default:
|
|
return fmt.Errorf("wait: unexpected packet %T received: %v", msg, msg)
|
|
}
|
|
}
|
|
if wm.status == 0 {
|
|
return nil
|
|
}
|
|
if wm.status == -1 {
|
|
// exit-status was never sent from server
|
|
if wm.signal == "" {
|
|
return errors.New("wait: remote command exited without exit status or exit signal")
|
|
}
|
|
wm.status = 128
|
|
if _, ok := signals[Signal(wm.signal)]; ok {
|
|
wm.status += signals[Signal(wm.signal)]
|
|
}
|
|
}
|
|
return &ExitError{wm}
|
|
}
|
|
|
|
func (s *Session) stdin() {
|
|
if s.stdinpipe {
|
|
return
|
|
}
|
|
if s.Stdin == nil {
|
|
s.Stdin = new(bytes.Buffer)
|
|
}
|
|
s.copyFuncs = append(s.copyFuncs, func() error {
|
|
_, err := io.Copy(s.clientChan.stdin, s.Stdin)
|
|
if err1 := s.clientChan.stdin.Close(); err == nil {
|
|
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.clientChan.stdout)
|
|
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.clientChan.stderr)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// 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 s.clientChan.stdin, 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 enought 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.clientChan.stdout, 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 enought 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.clientChan.stderr, nil
|
|
}
|
|
|
|
// TODO(dfc) add Output and CombinedOutput helpers
|
|
|
|
// NewSession returns a new interactive session on the remote host.
|
|
func (c *ClientConn) NewSession() (*Session, error) {
|
|
ch := c.newChan(c.transport)
|
|
if err := c.writePacket(marshal(msgChannelOpen, channelOpenMsg{
|
|
ChanType: "session",
|
|
PeersId: ch.id,
|
|
PeersWindow: 1 << 14,
|
|
MaxPacketSize: 1 << 15, // RFC 4253 6.1
|
|
})); err != nil {
|
|
c.chanlist.remove(ch.id)
|
|
return nil, err
|
|
}
|
|
if err := ch.waitForChannelOpenResponse(); err != nil {
|
|
c.chanlist.remove(ch.id)
|
|
return nil, fmt.Errorf("ssh: unable to open session: %v", err)
|
|
}
|
|
return &Session{
|
|
clientChan: ch,
|
|
}, 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 {
|
|
return fmt.Sprintf("Process exited with: %v. Reason was: %v (%v)", w.status, w.msg, w.signal)
|
|
}
|