RFC 4254 s5.2 is clear that a client must never send a data packet larger than the value of maximum packet supplied by the remote side during channel setup. The client was not honoring this value, in fact it wasn't even recording it. Thanks to Albert Strasheim for the bug report. R=agl, fullung CC=golang-dev https://golang.org/cl/6448128
490 lines
12 KiB
Go
490 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 tests.
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"testing"
|
|
|
|
"code.google.com/p/go.crypto/ssh/terminal"
|
|
)
|
|
|
|
type serverType func(*serverChan)
|
|
|
|
// dial constructs a new test server and returns a *ClientConn.
|
|
func dial(handler serverType, t *testing.T) *ClientConn {
|
|
pw := password("tiger")
|
|
serverConfig.PasswordCallback = func(conn *ServerConn, user, pass string) bool {
|
|
return user == "testuser" && pass == string(pw)
|
|
}
|
|
serverConfig.PublicKeyCallback = nil
|
|
|
|
l, err := Listen("tcp", "127.0.0.1:0", serverConfig)
|
|
if err != nil {
|
|
t.Fatalf("unable to listen: %s", err)
|
|
}
|
|
go func() {
|
|
defer l.Close()
|
|
conn, err := l.Accept()
|
|
if err != nil {
|
|
t.Errorf("Unable to accept: %v", err)
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
if err := conn.Handshake(); err != nil {
|
|
t.Errorf("Unable to handshake: %v", err)
|
|
return
|
|
}
|
|
for {
|
|
ch, err := conn.Accept()
|
|
if err == io.EOF {
|
|
return
|
|
}
|
|
// We sometimes get ECONNRESET rather than EOF.
|
|
if _, ok := err.(*net.OpError); ok {
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("Unable to accept incoming channel request: %v", err)
|
|
return
|
|
}
|
|
if ch.ChannelType() != "session" {
|
|
ch.Reject(UnknownChannelType, "unknown channel type")
|
|
continue
|
|
}
|
|
ch.Accept()
|
|
go handler(ch.(*serverChan))
|
|
}
|
|
t.Log("done")
|
|
}()
|
|
|
|
config := &ClientConfig{
|
|
User: "testuser",
|
|
Auth: []ClientAuth{
|
|
ClientAuthPassword(pw),
|
|
},
|
|
}
|
|
|
|
c, err := Dial("tcp", l.Addr().String(), config)
|
|
if err != nil {
|
|
t.Fatalf("unable to dial remote side: %s", err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// Test a simple string is returned to session.Stdout.
|
|
func TestSessionShell(t *testing.T) {
|
|
conn := dial(shellHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
stdout := new(bytes.Buffer)
|
|
session.Stdout = stdout
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
if err := session.Wait(); err != nil {
|
|
t.Fatalf("Remote command did not exit cleanly: %s", err)
|
|
}
|
|
actual := stdout.String()
|
|
if actual != "golang" {
|
|
t.Fatalf("Remote shell did not return expected string: expected=golang, actual=%s", actual)
|
|
}
|
|
}
|
|
|
|
// TODO(dfc) add support for Std{in,err}Pipe when the Server supports it.
|
|
|
|
// Test a simple string is returned via StdoutPipe.
|
|
func TestSessionStdoutPipe(t *testing.T) {
|
|
conn := dial(shellHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
stdout, err := session.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request StdoutPipe(): %v", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
if _, err := io.Copy(&buf, stdout); err != nil {
|
|
t.Errorf("Copy of stdout failed: %v", err)
|
|
}
|
|
done <- true
|
|
}()
|
|
if err := session.Wait(); err != nil {
|
|
t.Fatalf("Remote command did not exit cleanly: %s", err)
|
|
}
|
|
<-done
|
|
actual := buf.String()
|
|
if actual != "golang" {
|
|
t.Fatalf("Remote shell did not return expected string: expected=golang, actual=%s", actual)
|
|
}
|
|
}
|
|
|
|
// Test non-0 exit status is returned correctly.
|
|
func TestExitStatusNonZero(t *testing.T) {
|
|
conn := dial(exitStatusNonZeroHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err == nil {
|
|
t.Fatalf("expected command to fail but it didn't")
|
|
}
|
|
e, ok := err.(*ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *ExitError but got %T", err)
|
|
}
|
|
if e.ExitStatus() != 15 {
|
|
t.Fatalf("expected command to exit with 15 but got %s", e.ExitStatus())
|
|
}
|
|
}
|
|
|
|
// Test 0 exit status is returned correctly.
|
|
func TestExitStatusZero(t *testing.T) {
|
|
conn := dial(exitStatusZeroHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err != nil {
|
|
t.Fatalf("expected nil but got %s", err)
|
|
}
|
|
}
|
|
|
|
// Test exit signal and status are both returned correctly.
|
|
func TestExitSignalAndStatus(t *testing.T) {
|
|
conn := dial(exitSignalAndStatusHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err == nil {
|
|
t.Fatalf("expected command to fail but it didn't")
|
|
}
|
|
e, ok := err.(*ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *ExitError but got %T", err)
|
|
}
|
|
if e.Signal() != "TERM" || e.ExitStatus() != 15 {
|
|
t.Fatalf("expected command to exit with signal TERM and status 15 but got signal %s and status %v", e.Signal(), e.ExitStatus())
|
|
}
|
|
}
|
|
|
|
// Test exit signal and status are both returned correctly.
|
|
func TestKnownExitSignalOnly(t *testing.T) {
|
|
conn := dial(exitSignalHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err == nil {
|
|
t.Fatalf("expected command to fail but it didn't")
|
|
}
|
|
e, ok := err.(*ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *ExitError but got %T", err)
|
|
}
|
|
if e.Signal() != "TERM" || e.ExitStatus() != 143 {
|
|
t.Fatalf("expected command to exit with signal TERM and status 143 but got signal %s and status %v", e.Signal(), e.ExitStatus())
|
|
}
|
|
}
|
|
|
|
// Test exit signal and status are both returned correctly.
|
|
func TestUnknownExitSignal(t *testing.T) {
|
|
conn := dial(exitSignalUnknownHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err == nil {
|
|
t.Fatalf("expected command to fail but it didn't")
|
|
}
|
|
e, ok := err.(*ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *ExitError but got %T", err)
|
|
}
|
|
if e.Signal() != "SYS" || e.ExitStatus() != 128 {
|
|
t.Fatalf("expected command to exit with signal SYS and status 128 but got signal %s and status %v", e.Signal(), e.ExitStatus())
|
|
}
|
|
}
|
|
|
|
// Test WaitMsg is not returned if the channel closes abruptly.
|
|
func TestExitWithoutStatusOrSignal(t *testing.T) {
|
|
conn := dial(exitWithoutSignalOrStatus, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err == nil {
|
|
t.Fatalf("expected command to fail but it didn't")
|
|
}
|
|
_, ok := err.(*ExitError)
|
|
if ok {
|
|
// you can't actually test for errors.errorString
|
|
// because it's not exported.
|
|
t.Fatalf("expected *errorString but got %T", err)
|
|
}
|
|
}
|
|
|
|
func TestInvalidServerMessage(t *testing.T) {
|
|
conn := dial(sendInvalidRecord, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
// Make sure that we closed all the clientChans when the connection
|
|
// failed.
|
|
session.wait()
|
|
|
|
defer session.Close()
|
|
}
|
|
|
|
// In the wild some clients (and servers) send zero sized window updates.
|
|
// Test that the client can continue after receiving a zero sized update.
|
|
func TestClientZeroWindowAdjust(t *testing.T) {
|
|
conn := dial(sendZeroWindowAdjust, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
err = session.Wait()
|
|
if err != nil {
|
|
t.Fatalf("expected nil but got %s", err)
|
|
}
|
|
}
|
|
|
|
// In the wild some clients (and servers) send zero sized window updates.
|
|
// Test that the server can continue after receiving a zero size update.
|
|
func TestServerZeroWindowAdjust(t *testing.T) {
|
|
conn := dial(exitStatusZeroHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
|
|
// send a bogus zero sized window update
|
|
session.clientChan.sendWindowAdj(0)
|
|
|
|
err = session.Wait()
|
|
if err != nil {
|
|
t.Fatalf("expected nil but got %s", err)
|
|
}
|
|
}
|
|
|
|
// Verify that we never send a packet larger than maxpacket.
|
|
func TestClientStdinRespectsMaxPacketSize(t *testing.T) {
|
|
conn := dial(discardHandler, t)
|
|
defer conn.Close()
|
|
session, err := conn.NewSession()
|
|
if err != nil {
|
|
t.Fatalf("Unable to request new session: %s", err)
|
|
}
|
|
defer session.Close()
|
|
if err := session.Shell(); err != nil {
|
|
t.Fatalf("Unable to execute command: %s", err)
|
|
}
|
|
// try to stuff 128k of data into a 32k hole.
|
|
const size = 128 * 1024
|
|
n, err := session.clientChan.stdin.Write(make([]byte, size))
|
|
if n != size || err != nil {
|
|
t.Fatalf("failed to write: %d, %v", n, err)
|
|
}
|
|
}
|
|
|
|
type exitStatusMsg struct {
|
|
PeersId uint32
|
|
Request string
|
|
WantReply bool
|
|
Status uint32
|
|
}
|
|
|
|
type exitSignalMsg struct {
|
|
PeersId uint32
|
|
Request string
|
|
WantReply bool
|
|
Signal string
|
|
CoreDumped bool
|
|
Errmsg string
|
|
Lang string
|
|
}
|
|
|
|
func newServerShell(ch *serverChan, prompt string) *ServerTerminal {
|
|
term := terminal.NewTerminal(ch, prompt)
|
|
return &ServerTerminal{
|
|
Term: term,
|
|
Channel: ch,
|
|
}
|
|
}
|
|
|
|
func exitStatusZeroHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
// this string is returned to stdout
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
sendStatus(0, ch)
|
|
}
|
|
|
|
func exitStatusNonZeroHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
sendStatus(15, ch)
|
|
}
|
|
|
|
func exitSignalAndStatusHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
sendStatus(15, ch)
|
|
sendSignal("TERM", ch)
|
|
}
|
|
|
|
func exitSignalHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
sendSignal("TERM", ch)
|
|
}
|
|
|
|
func exitSignalUnknownHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
sendSignal("SYS", ch)
|
|
}
|
|
|
|
func exitWithoutSignalOrStatus(ch *serverChan) {
|
|
defer ch.Close()
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
}
|
|
|
|
func shellHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
// this string is returned to stdout
|
|
shell := newServerShell(ch, "golang")
|
|
shell.ReadLine()
|
|
sendStatus(0, ch)
|
|
}
|
|
|
|
func sendStatus(status uint32, ch *serverChan) {
|
|
msg := exitStatusMsg{
|
|
PeersId: ch.remoteId,
|
|
Request: "exit-status",
|
|
WantReply: false,
|
|
Status: status,
|
|
}
|
|
ch.serverConn.writePacket(marshal(msgChannelRequest, msg))
|
|
}
|
|
|
|
func sendSignal(signal string, ch *serverChan) {
|
|
sig := exitSignalMsg{
|
|
PeersId: ch.remoteId,
|
|
Request: "exit-signal",
|
|
WantReply: false,
|
|
Signal: signal,
|
|
CoreDumped: false,
|
|
Errmsg: "Process terminated",
|
|
Lang: "en-GB-oed",
|
|
}
|
|
ch.serverConn.writePacket(marshal(msgChannelRequest, sig))
|
|
}
|
|
|
|
func sendInvalidRecord(ch *serverChan) {
|
|
defer ch.Close()
|
|
packet := make([]byte, 1+4+4+1)
|
|
packet[0] = msgChannelData
|
|
marshalUint32(packet[1:], 29348723 /* invalid channel id */)
|
|
marshalUint32(packet[5:], 1)
|
|
packet[9] = 42
|
|
|
|
ch.serverConn.writePacket(packet)
|
|
}
|
|
|
|
func sendZeroWindowAdjust(ch *serverChan) {
|
|
defer ch.Close()
|
|
// send a bogus zero sized window update
|
|
ch.sendWindowAdj(0)
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
sendStatus(0, ch)
|
|
}
|
|
|
|
func discardHandler(ch *serverChan) {
|
|
defer ch.Close()
|
|
// grow the window to avoid being fooled by
|
|
// the initial 1 << 14 window.
|
|
ch.sendWindowAdj(1024 * 1024)
|
|
shell := newServerShell(ch, "> ")
|
|
shell.ReadLine()
|
|
io.Copy(ioutil.Discard, ch.serverConn)
|
|
}
|