f555e7a9f2
* I had to contextualise Communicator.Start and RemoteCmd.StartWithUi NOTE: Communicator.Start starts a RemoteCmd but RemoteCmd.StartWithUi will run the cmd and wait for a return, so I renamed StartWithUi to RunWithUi so that the intent is clearer. Ideally in the future RunWithUi will be named back to StartWithUi and the exit status or wait funcs of the command will allow to wait for a return. If you do so please read carrefully https://golang.org/pkg/os/exec/#Cmd.Stdout to avoid a deadlock * cmd.ExitStatus to cmd.ExitStatus() is now blocking to avoid race conditions * also had to simplify StartWithUi
230 lines
6.1 KiB
Go
230 lines
6.1 KiB
Go
// +build !race
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/packer/packer"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// private key for mock server
|
|
const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU
|
|
70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx
|
|
9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF
|
|
tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z
|
|
s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc
|
|
qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT
|
|
+IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea
|
|
riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH
|
|
D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh
|
|
atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT
|
|
b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN
|
|
ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M
|
|
MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4
|
|
KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8
|
|
e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1
|
|
D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+
|
|
3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj
|
|
orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw
|
|
64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc
|
|
XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc
|
|
QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g
|
|
/SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ
|
|
I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk
|
|
gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl
|
|
NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw==
|
|
-----END RSA PRIVATE KEY-----`
|
|
|
|
var serverConfig = &ssh.ServerConfig{
|
|
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
if c.User() == "user" && string(pass) == "pass" {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
// Parse and set the private key of the server, required to accept connections
|
|
signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
|
|
if err != nil {
|
|
panic("unable to parse private key: " + err.Error())
|
|
}
|
|
serverConfig.AddHostKey(signer)
|
|
}
|
|
|
|
func newMockLineServer(t *testing.T) string {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Unable to listen for connection: %s", err)
|
|
}
|
|
|
|
go func() {
|
|
defer l.Close()
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
t.Errorf("Unable to accept incoming connection: %s", err)
|
|
}
|
|
defer c.Close()
|
|
conn, chans, _, err := ssh.NewServerConn(c, serverConfig)
|
|
if err != nil {
|
|
t.Logf("Handshaking error: %v", err)
|
|
}
|
|
t.Log("Accepted SSH connection")
|
|
for newChannel := range chans {
|
|
channel, _, err := newChannel.Accept()
|
|
if err != nil {
|
|
t.Errorf("Unable to accept channel.")
|
|
}
|
|
t.Log("Accepted channel")
|
|
|
|
go func(channelType string) {
|
|
defer channel.Close()
|
|
conn.OpenChannel(channelType, nil)
|
|
}(newChannel.ChannelType())
|
|
}
|
|
conn.Close()
|
|
}()
|
|
|
|
return l.Addr().String()
|
|
}
|
|
|
|
func newMockBrokenServer(t *testing.T) string {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Unable tp listen for connection: %s", err)
|
|
}
|
|
|
|
go func() {
|
|
defer l.Close()
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
t.Errorf("Unable to accept incoming connection: %s", err)
|
|
}
|
|
defer c.Close()
|
|
// This should block for a period of time longer than our timeout in
|
|
// the test case. That way we invoke a failure scenario.
|
|
t.Log("Block on handshaking for SSH connection")
|
|
time.Sleep(5 * time.Second)
|
|
}()
|
|
|
|
return l.Addr().String()
|
|
}
|
|
|
|
func TestCommIsCommunicator(t *testing.T) {
|
|
var raw interface{}
|
|
raw = &comm{}
|
|
if _, ok := raw.(packer.Communicator); !ok {
|
|
t.Fatalf("comm must be a communicator")
|
|
}
|
|
}
|
|
|
|
func TestNew_Invalid(t *testing.T) {
|
|
clientConfig := &ssh.ClientConfig{
|
|
User: "user",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password("i-am-invalid"),
|
|
},
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
address := newMockLineServer(t)
|
|
conn := func() (net.Conn, error) {
|
|
conn, err := net.Dial("tcp", address)
|
|
if err != nil {
|
|
t.Errorf("Unable to accept incoming connection: %v", err)
|
|
}
|
|
return conn, err
|
|
}
|
|
|
|
config := &Config{
|
|
Connection: conn,
|
|
SSHConfig: clientConfig,
|
|
}
|
|
|
|
_, err := New(address, config)
|
|
if err == nil {
|
|
t.Fatal("should have had an error connecting")
|
|
}
|
|
}
|
|
|
|
func TestStart(t *testing.T) {
|
|
clientConfig := &ssh.ClientConfig{
|
|
User: "user",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password("pass"),
|
|
},
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
address := newMockLineServer(t)
|
|
conn := func() (net.Conn, error) {
|
|
conn, err := net.Dial("tcp", address)
|
|
if err != nil {
|
|
t.Fatalf("unable to dial to remote side: %s", err)
|
|
}
|
|
return conn, err
|
|
}
|
|
|
|
config := &Config{
|
|
Connection: conn,
|
|
SSHConfig: clientConfig,
|
|
}
|
|
|
|
client, err := New(address, config)
|
|
if err != nil {
|
|
t.Fatalf("error connecting to SSH: %s", err)
|
|
}
|
|
|
|
cmd := &packer.RemoteCmd{
|
|
Command: "echo foo",
|
|
Stdout: new(bytes.Buffer),
|
|
}
|
|
|
|
ctx := context.Background()
|
|
client.Start(ctx, cmd)
|
|
}
|
|
|
|
func TestHandshakeTimeout(t *testing.T) {
|
|
clientConfig := &ssh.ClientConfig{
|
|
User: "user",
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password("pass"),
|
|
},
|
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
|
}
|
|
|
|
address := newMockBrokenServer(t)
|
|
conn := func() (net.Conn, error) {
|
|
conn, err := net.Dial("tcp", address)
|
|
if err != nil {
|
|
t.Fatalf("unable to dial to remote side: %s", err)
|
|
}
|
|
return conn, err
|
|
}
|
|
|
|
config := &Config{
|
|
Connection: conn,
|
|
SSHConfig: clientConfig,
|
|
HandshakeTimeout: 50 * time.Millisecond,
|
|
}
|
|
|
|
_, err := New(address, config)
|
|
if err != ErrHandshakeTimeout {
|
|
// Note: there's another error that can come back from this call:
|
|
// ssh: handshake failed: EOF
|
|
// This should appear in cases where the handshake fails because of
|
|
// malformed (or no) data sent back by the server, but should not happen
|
|
// in a timeout scenario.
|
|
t.Fatalf("Expected handshake timeout, got: %s", err)
|
|
}
|
|
}
|