mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-20 02:09:26 -05:00
Go 1.17 and earlier used a different syntax for build constraint comments, starting with "+build". Go 1.18 changed this to the modern "go:build" form as part of standardizing the structure of toolchain directive comments, and so for a while it was convention to include comments in both styles to allow building with both old and new Go compilers. However, Go 1.17 is no longer supported, and regardless of that we only expect OpenTofu to be built with the specific version we have selected in "go.mod" and ".go-version" anyway, so we no longer need the legacy form of these comments: the all supported Go toolchains now support the new form, which this commit retains. golangci-lint v2.6.0 includes a check for this legacy form, so removing this will also allow us to upgrade to a newer version of that linter aggregator in a future commit. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
846 lines
26 KiB
Go
846 lines
26 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
//go:build !race
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/opentofu/opentofu/internal/communicator/remote"
|
|
)
|
|
|
|
// 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-----`
|
|
|
|
// this cert was signed by the key from testCAPublicKey
|
|
const testServerHostCert = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgvQ3Bs1ex7277b9q6I0fNaWsVEC16f+LcT8RLPSVMEVMAAAADAQABAAABAQDX2UZWxOohPmKI1hGCehjULCRsRNblyr5HOTm/+ROV/fVelJTvQdVaRtMREQKNph1czaAZxtv6zGmroa1d/UzeRWibJyqHHCE+/gKvpenhZP+OQXH3P4UXOl6h0YlaM4fovYfm5fUK+v0QN1Cn2338nfb+oEWe1jwbChQj/L/UxJOYyIW26l0w4M3Tri93eDIwpPCuVDy1kzppi7I4+y60uVRjsznHkXAwNi+c8NJ7JP8jDTOzcH40LKp54x3ZPtjNAWdEBOPQzuszkuhKzsNWpWuI4QAGywXIuPfU9uhqguE4qByqgz2SGQ3OvsUdW+L4OFgzaMPQPC+pks3o2acvAAAAAAAAAAAAAAACAAAAB2NhLXRlc3QAAAANAAAACTEyNy4wLjAuMQAAAABag0jkAAAAAHDcHtAAAAAAAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQCrozyZIhdEvalCn+eSzHH94cO9ykiywA13ntWI7mJcHBwYTeCYWG8E9zGXyp2iDOjCGudM0Tdt8o0OofKChk9Z/qiUN0G8y1kmaXBlBM3qA5R9NPpvMYMNkYLfX6ivtZCnqrsbzaoqN2Oc/7H2StHzJWh/XCGu9otQZA6vdv1oSmAsZOjw/xIGaGQqDUaLq21J280PP1qSbdJHf76iSHE+TWe3YpqV946JWM5tCh0DykZ10VznvxYpUjzhr07IN3tVKxOXbPnnU7lX6IaLIWgfzLqwSyheeux05c3JLF9iF4sFu8ou4hwQz1iuUTU1jxgwZP0w/bkXgFFs0949lW81AAABDwAAAAdzc2gtcnNhAAABAEyoiVkZ5z79nh3WSU5mU2U7e2BItnnEqsJIm9EN+35uG0yORSXmQoaa9mtli7G3r79tyqEJd/C95EdNvU/9TjaoDcbH8OHP+Ue9XSfUzBuQ6bGSXe6mlZlO7QJ1cIyWphFP3MkrweDSiJ+SpeXzLzZkiJ7zKv5czhBEyG/MujFgvikotL+eUNG42y2cgsesXSjENSBS3l11q55a+RM2QKt3W32im8CsSxrH6Mz6p4JXQNgsVvZRknLxNlWXULFB2HLTunPKzJNMTf6xZf66oivSBAXVIdNKhlVpAQ3dT/dW5K6J4aQF/hjWByyLprFwZ16cPDqvtalnTCpbRYelNbw=`
|
|
|
|
const testCAPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrozyZIhdEvalCn+eSzHH94cO9ykiywA13ntWI7mJcHBwYTeCYWG8E9zGXyp2iDOjCGudM0Tdt8o0OofKChk9Z/qiUN0G8y1kmaXBlBM3qA5R9NPpvMYMNkYLfX6ivtZCnqrsbzaoqN2Oc/7H2StHzJWh/XCGu9otQZA6vdv1oSmAsZOjw/xIGaGQqDUaLq21J280PP1qSbdJHf76iSHE+TWe3YpqV946JWM5tCh0DykZ10VznvxYpUjzhr07IN3tVKxOXbPnnU7lX6IaLIWgfzLqwSyheeux05c3JLF9iF4sFu8ou4hwQz1iuUTU1jxgwZP0w/bkXgFFs0949lW81`
|
|
|
|
func newMockLineServer(t *testing.T, signer ssh.Signer, pubKey string) (string, func()) {
|
|
serverConfig := &ssh.ServerConfig{
|
|
PasswordCallback: acceptUserPass("user", "pass"),
|
|
PublicKeyCallback: acceptPublicKey(pubKey),
|
|
}
|
|
|
|
var err error
|
|
if signer == nil {
|
|
signer, err = ssh.ParsePrivateKey([]byte(testServerPrivateKey))
|
|
if err != nil {
|
|
t.Fatalf("unable to parse private key: %s", err)
|
|
}
|
|
}
|
|
serverConfig.AddHostKey(signer)
|
|
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Unable to listen for connection: %s", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
go func() {
|
|
defer l.Close()
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
t.Errorf("Unable to accept incoming connection: %s", err)
|
|
}
|
|
defer c.Close()
|
|
_, chans, _, err := ssh.NewServerConn(c, serverConfig)
|
|
if err != nil {
|
|
t.Logf("Handshaking error: %v", err)
|
|
}
|
|
t.Log("Accepted SSH connection")
|
|
|
|
for newChannel := range chans {
|
|
wg.Add(1)
|
|
channel, requests, err := newChannel.Accept()
|
|
if err != nil {
|
|
t.Errorf("Unable to accept channel.")
|
|
}
|
|
t.Log("Accepted channel")
|
|
|
|
go func(in <-chan *ssh.Request) {
|
|
defer channel.Close()
|
|
defer wg.Done()
|
|
|
|
for req := range in {
|
|
t.Log("Start request")
|
|
// since this channel's requests are serviced serially,
|
|
// this will block keepalive probes, and can simulate a
|
|
// hung connection.
|
|
if bytes.Contains(req.Payload, []byte("sleep")) {
|
|
t.Log("Sleep")
|
|
time.Sleep(time.Second)
|
|
}
|
|
|
|
if req.WantReply {
|
|
t.Log("Reply")
|
|
if err := req.Reply(true, nil); err != nil {
|
|
// For now, we are allowing this reply error for the TestFailedKeepAlives test as the connection disappears between sleep and reply
|
|
t.Log("Ignoring: " + err.Error())
|
|
}
|
|
}
|
|
}
|
|
}(requests)
|
|
}
|
|
}()
|
|
|
|
return l.Addr().String(), func() { wg.Wait() }
|
|
}
|
|
|
|
func TestNew_Invalid(t *testing.T) {
|
|
address, sclose := newMockLineServer(t, nil, testClientPublicKey)
|
|
defer sclose()
|
|
parts := strings.Split(address, ":")
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"password": cty.StringVal("i-am-invalid"),
|
|
"host": cty.StringVal(parts[0]),
|
|
"port": cty.StringVal(parts[1]),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
|
|
err = c.Connect(nil)
|
|
if err == nil {
|
|
t.Fatal("should have had an error connecting")
|
|
}
|
|
}
|
|
|
|
func TestNew_InvalidHost(t *testing.T) {
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"password": cty.StringVal("i-am-invalid"),
|
|
"port": cty.StringVal("22"),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
_, err := New(v)
|
|
if err == nil {
|
|
t.Fatal("should have had an error creating communicator")
|
|
}
|
|
}
|
|
|
|
func TestNew_PublicKeyAsCertificate(t *testing.T) {
|
|
// This test ensures that we correctly identify and reject a public key
|
|
// in the "certificate" argument, which historically caused a panic
|
|
// as described in https://github.com/opentofu/opentofu/issues/3369 .
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"host": cty.StringVal("example.com"),
|
|
"port": cty.StringVal("22"),
|
|
"timeout": cty.StringVal("30s"),
|
|
|
|
// The specific private key used here is unimportant for this test,
|
|
// but we do need to provide one because otherwise the "certificate"
|
|
// argument is silently ignored.
|
|
"private_key": cty.StringVal(testServerPrivateKey),
|
|
// "certificate" uses the same syntax as an SSH public key, but _must_
|
|
// use one of the SSH certificate formats, rather than a plain public
|
|
// key like in this example.
|
|
"certificate": cty.StringVal("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKrMq5j3tsD0M+ceNKXVCRIF7De7iz0CKQqxoiyZFEeR example"),
|
|
})
|
|
|
|
_, err := New(v)
|
|
if err == nil {
|
|
t.Fatal("should have had an error creating communicator")
|
|
}
|
|
got := err.Error()
|
|
want := `invalid certificate format "ssh-ed25519": must use a certificate entry type, or leave certificate unset if you don't intend to use a certificate authority`
|
|
if got != want {
|
|
t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestStart(t *testing.T) {
|
|
address, sclose := newMockLineServer(t, nil, testClientPublicKey)
|
|
defer sclose()
|
|
parts := strings.Split(address, ":")
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"password": cty.StringVal("pass"),
|
|
"host": cty.StringVal(parts[0]),
|
|
"port": cty.StringVal(parts[1]),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
defer func() {
|
|
if err := c.Disconnect(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "echo foo"
|
|
cmd.Stdout = stdout
|
|
|
|
err = c.Start(&cmd)
|
|
if err != nil {
|
|
t.Fatalf("error executing remote command: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestKeepAlives verifies that the keepalive messages don't interfere with
|
|
// normal operation of the client.
|
|
func TestKeepAlives(t *testing.T) {
|
|
ivl := keepAliveInterval
|
|
keepAliveInterval = 250 * time.Millisecond
|
|
defer func() { keepAliveInterval = ivl }()
|
|
|
|
address, sclose := newMockLineServer(t, nil, testClientPublicKey)
|
|
defer sclose()
|
|
parts := strings.Split(address, ":")
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"password": cty.StringVal("pass"),
|
|
"host": cty.StringVal(parts[0]),
|
|
"port": cty.StringVal(parts[1]),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
defer func() {
|
|
if err := c.Disconnect(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
if err := c.Connect(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "sleep"
|
|
cmd.Stdout = stdout
|
|
|
|
// wait a bit before executing the command, so that at least 1 keepalive is sent
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
err = c.Start(&cmd)
|
|
if err != nil {
|
|
t.Fatalf("error executing remote command: %s", err)
|
|
}
|
|
}
|
|
|
|
// TestFailedKeepAlives verifies that failed keepalive messages will eventually
|
|
// kill the connection.
|
|
func TestFailedKeepAlives(t *testing.T) {
|
|
ivl := keepAliveInterval
|
|
del := maxKeepAliveDelay
|
|
maxKeepAliveDelay = 500 * time.Millisecond
|
|
keepAliveInterval = 250 * time.Millisecond
|
|
defer func() {
|
|
keepAliveInterval = ivl
|
|
maxKeepAliveDelay = del
|
|
}()
|
|
|
|
address, sclose := newMockLineServer(t, nil, testClientPublicKey)
|
|
defer sclose()
|
|
parts := strings.Split(address, ":")
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"password": cty.StringVal("pass"),
|
|
"host": cty.StringVal(parts[0]),
|
|
"port": cty.StringVal(parts[1]),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
defer func() {
|
|
if err := c.Disconnect(); err == nil {
|
|
t.Fatal("Disconnect error expected")
|
|
}
|
|
}()
|
|
|
|
if err := c.Connect(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "sleep"
|
|
cmd.Stdout = stdout
|
|
|
|
err = c.Start(&cmd)
|
|
if err == nil {
|
|
t.Fatal("expected connection error")
|
|
}
|
|
}
|
|
|
|
func TestLostConnection(t *testing.T) {
|
|
address, sclose := newMockLineServer(t, nil, testClientPublicKey)
|
|
defer sclose()
|
|
parts := strings.Split(address, ":")
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal("user"),
|
|
"password": cty.StringVal("pass"),
|
|
"host": cty.StringVal(parts[0]),
|
|
"port": cty.StringVal(parts[1]),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
defer func() {
|
|
// On Darwin systems it seems that calling Disconnect on a closed
|
|
// connection will return an error. This is not the case on other platforms.
|
|
if err := c.Disconnect(); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "echo foo"
|
|
cmd.Stdout = stdout
|
|
|
|
err = c.Start(&cmd)
|
|
if err != nil {
|
|
t.Fatalf("error executing remote command: %s", err)
|
|
}
|
|
|
|
// The test server can't execute anything, so Wait will block, unless
|
|
// there's an error. Disconnect the communicator transport, to cause the
|
|
// command to fail.
|
|
go func() {
|
|
time.Sleep(100 * time.Millisecond)
|
|
if err := c.Disconnect(); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
err = cmd.Wait()
|
|
if err == nil {
|
|
t.Fatal("expected communicator error")
|
|
}
|
|
}
|
|
|
|
func TestHostKey(t *testing.T) {
|
|
// get the server's public key
|
|
signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
|
|
if err != nil {
|
|
t.Fatalf("unable to parse private key: %v", err)
|
|
}
|
|
pubKey := fmt.Sprintf("ssh-rsa %s", base64.StdEncoding.EncodeToString(signer.PublicKey().Marshal()))
|
|
|
|
address, sclose := newMockLineServer(t, nil, testClientPublicKey)
|
|
host, p, _ := net.SplitHostPort(address)
|
|
port, _ := strconv.Atoi(p)
|
|
|
|
connInfo := &connectionInfo{
|
|
User: "user",
|
|
Password: "pass",
|
|
Host: host,
|
|
HostKey: pubKey,
|
|
Port: uint16(port),
|
|
Timeout: "30s",
|
|
}
|
|
|
|
cfg, err := prepareSSHConfig(connInfo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := &Communicator{
|
|
connInfo: connInfo,
|
|
config: cfg,
|
|
}
|
|
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "echo foo"
|
|
cmd.Stdout = stdout
|
|
|
|
if err := c.Start(&cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.Disconnect(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sclose()
|
|
|
|
// now check with the wrong HostKey
|
|
address, sclose = newMockLineServer(t, nil, testClientPublicKey)
|
|
defer sclose()
|
|
_, p, _ = net.SplitHostPort(address)
|
|
port, _ = strconv.Atoi(p)
|
|
|
|
connInfo.HostKey = testClientPublicKey
|
|
connInfo.Port = uint16(port)
|
|
|
|
cfg, err = prepareSSHConfig(connInfo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c = &Communicator{
|
|
connInfo: connInfo,
|
|
config: cfg,
|
|
}
|
|
|
|
err = c.Start(&cmd)
|
|
if err == nil || !strings.Contains(err.Error(), "mismatch") {
|
|
t.Fatalf("expected host key mismatch, got error:%v", err)
|
|
}
|
|
}
|
|
|
|
func TestHostCert(t *testing.T) {
|
|
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(testServerHostCert))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
signer, err = ssh.NewCertSigner(pk.(*ssh.Certificate), signer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
address, sclose := newMockLineServer(t, signer, testClientPublicKey)
|
|
host, p, _ := net.SplitHostPort(address)
|
|
port, _ := strconv.Atoi(p)
|
|
|
|
connInfo := &connectionInfo{
|
|
User: "user",
|
|
Password: "pass",
|
|
Host: host,
|
|
HostKey: testCAPublicKey,
|
|
Port: uint16(port),
|
|
Timeout: "30s",
|
|
}
|
|
|
|
cfg, err := prepareSSHConfig(connInfo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := &Communicator{
|
|
connInfo: connInfo,
|
|
config: cfg,
|
|
}
|
|
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "echo foo"
|
|
cmd.Stdout = stdout
|
|
|
|
if err := c.Start(&cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.Disconnect(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sclose()
|
|
|
|
// now check with the wrong HostKey
|
|
address, sclose = newMockLineServer(t, signer, testClientPublicKey)
|
|
defer sclose()
|
|
_, p, _ = net.SplitHostPort(address)
|
|
port, _ = strconv.Atoi(p)
|
|
|
|
connInfo.HostKey = testClientPublicKey
|
|
connInfo.Port = uint16(port)
|
|
|
|
cfg, err = prepareSSHConfig(connInfo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c = &Communicator{
|
|
connInfo: connInfo,
|
|
config: cfg,
|
|
}
|
|
|
|
err = c.Start(&cmd)
|
|
if err == nil || !strings.Contains(err.Error(), "authorities") {
|
|
t.Fatalf("expected host key mismatch, got error:%v", err)
|
|
}
|
|
}
|
|
|
|
const SERVER_PEM = `-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEpAIBAAKCAQEA8CkDr7uxCFt6lQUVwS8NyPO+fQNxORoGnMnN/XhVJZvpqyKR
|
|
Uji9R0d8D66bYxUUsabXjP2y4HTVzbZtnvXFZZshk0cOtJjjekpYJaLK2esPR/iX
|
|
wvSltNkrDQDPN/RmgEEMIevW8AgrPsqrnybFHxTpd7rEUHXBOe4nMNRIg3XHykB6
|
|
jZk8q5bBPUe3I/f0DK5TJEBpTc6dO3P/j93u55VUqr39/SPRHnld2mCw+c8v6UOh
|
|
sssO/DIZFPScD3DYqsk2N+/nz9zXfcOTdWGhawgxuIo1DTokrNQbG3pDrLqcWgqj
|
|
13vqJFCmRA0O2CQIwJePd6+Np/XO3Uh/KL6FlQIDAQABAoIBAQCmvQMXNmvCDqk7
|
|
30zsVDvw4fHGH+azK3Od1aqTqcEMHISOUbCtckFPxLzIsoSltRQqB1kuRVG07skm
|
|
Stsu+xny4lLcSwBVuLRuykEK2EyYIc/5Owo6y9pkhkaSf5ZfFes4bnD6+B/BhRpp
|
|
PRMMq0E+xCkX/G6iIi9mhgdlqm0x/vKtjzQeeshw9+gRcRLUpX+UeKFKXMXcDayx
|
|
qekr1bAaQKNBhTK+CbZjcqzG4f+BXVGRTZ9nsPAV+yTnWUCU0TghwPmtthHbebqa
|
|
9hlkum7qik/bQj/tjJ8/b0vTfHQSVxhtPG/ZV2Tn9ZuL/vrkYqeyMU8XkJ/uaEvH
|
|
WPyOcB4BAoGBAP5o5JSEtPog+U3JFrLNSRjz5ofZNVkJzice+0XyqlzJDHhX5tF8
|
|
mriYQZLLXYhckBm4IdkhTn/dVbXNQTzyy2WVuO5nU8bkCMvGL9CGpW4YGqwGf7NX
|
|
e4H3emtRjLv8VZpUHe/RUUDhmYvMSt1qmXuskfpROuGfLhQBUd6A4J+BAoGBAPGp
|
|
UcMKjrxZ5qjYU6DLgS+xeca4Eu70HgdbSQbRo45WubXjyXvTRFij36DrpxJWf1D7
|
|
lIsyBifoTra/lAuC1NQXGYWjTCdk2ey8Ll5qOgiXvE6lINHABr+U/Z90/g6LuML2
|
|
VzaZbq/QLcT3yVsdyTogKckzCaKsCpusyHE1CXAVAoGAd6kMglKc8N0bhZukgnsN
|
|
+5+UeacPcY6sGTh4RWErAjNKGzx1A2lROKvcg9gFaULoQECcIw2IZ5nKW5VsLueg
|
|
BWrTrcaJ4A2XmYjhKnp6SvspaGoyHD90hx/Iw7t6r1yzQsB3yDmytwqldtyjBdvC
|
|
zynPC2azhDWjraMlR7tka4ECgYAxwvLiHa9sm3qCtCDsUFtmrb3srITBjaUNUL/F
|
|
1q8+JR+Sk7gudj9xnTT0VvINNaB71YIt83wPBagHu4VJpYQbtDH+MbUBu6OgOtO1
|
|
f1w53rzY2OncJxV8p7pd9mJGLoE6LC2jQY7oRw7Vq0xcJdME1BCmrIrEY3a/vaF8
|
|
pjYuTQKBgQCIOH23Xita8KmhH0NdlWxZfcQt1j3AnOcKe6UyN4BsF8hqS7eTA52s
|
|
WjG5X2IBl7gs1eMM1qkqR8npS9nwfO/pBmZPwjiZoilypXxWj+c+P3vwre2yija4
|
|
bXgFVj4KFBwhr1+8KcobxC0SAPEouMvSkxzjjw+gnebozUtPlud9jA==
|
|
-----END RSA PRIVATE KEY-----
|
|
`
|
|
const CLIENT_CERT_SIGNED_BY_SERVER = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgbMDNUn4M2TtzrSH7MOT2QsvLzZWjehJ5TYrBOp9p+lwAAAADAQABAAABAQCyu57E7zIWRyEWuaiOiikOSZKFjbwLkpE9fboFfLLsNUJj4zw+5bZUJtzWK8roPjgL8s1oPncro5wuTtI2Nu4fkpeFK0Hb33o6Eyksuj4Om4+6Uemn1QEcb0bZqK8Zyg9Dg9deP7LeE0v78b5/jZafFgwxv+/sMhM0PRD34NCDYcYmkkHlvQtQWFAdbPXCgghObedZyYdoqZVuhTsiPMWtQS/cc9M4tv6mPOuQlhZt3R/Oh/kwUyu45oGRb5bhO4JicozFS3oeClpU+UMbgslkzApJqxZBWN7+PDFSZhKk2GslyeyP4sH3E30Z00yVi/lQYgmQsB+Hg6ClemNQMNu/AAAAAAAAAAAAAAACAAAABHVzZXIAAAAIAAAABHVzZXIAAAAAWzBjXAAAAAB/POfPAAAAAAAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEA8CkDr7uxCFt6lQUVwS8NyPO+fQNxORoGnMnN/XhVJZvpqyKRUji9R0d8D66bYxUUsabXjP2y4HTVzbZtnvXFZZshk0cOtJjjekpYJaLK2esPR/iXwvSltNkrDQDPN/RmgEEMIevW8AgrPsqrnybFHxTpd7rEUHXBOe4nMNRIg3XHykB6jZk8q5bBPUe3I/f0DK5TJEBpTc6dO3P/j93u55VUqr39/SPRHnld2mCw+c8v6UOhsssO/DIZFPScD3DYqsk2N+/nz9zXfcOTdWGhawgxuIo1DTokrNQbG3pDrLqcWgqj13vqJFCmRA0O2CQIwJePd6+Np/XO3Uh/KL6FlQAAAQ8AAAAHc3NoLXJzYQAAAQC6sKEQHyl954BQn2BXuTgOB3NkENBxN7SD8ZaS8PNkDESytLjSIqrzoE6m7xuzprA+G23XRrCY/um3UvM7+7+zbwig2NIBbGbp3QFliQHegQKW6hTZP09jAQZk5jRrrEr/QT/s+gtHPmjxJK7XOQYxhInDKj+aJg62ExcwpQlP/0ATKNOIkdzTzzq916p0UOnnVaaPMKibh5Lv69GafIhKJRZSuuLN9fvs1G1RuUbxn/BNSeoRCr54L++Ztg09fJxunoyELs8mwgzCgB3pdZoUR2Z6ak05W4mvH3lkSz2BKUrlwxI6mterxhJy1GuN1K/zBG0gEMl2UTLajGK3qKM8 itbitloaner@MacBook-Pro-4.fios-router.home`
|
|
const CLIENT_PEM = `-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEpAIBAAKCAQEAsruexO8yFkchFrmojoopDkmShY28C5KRPX26BXyy7DVCY+M8
|
|
PuW2VCbc1ivK6D44C/LNaD53K6OcLk7SNjbuH5KXhStB2996OhMpLLo+DpuPulHp
|
|
p9UBHG9G2aivGcoPQ4PXXj+y3hNL+/G+f42WnxYMMb/v7DITND0Q9+DQg2HGJpJB
|
|
5b0LUFhQHWz1woIITm3nWcmHaKmVboU7IjzFrUEv3HPTOLb+pjzrkJYWbd0fzof5
|
|
MFMruOaBkW+W4TuCYnKMxUt6HgpaVPlDG4LJZMwKSasWQVje/jwxUmYSpNhrJcns
|
|
j+LB9xN9GdNMlYv5UGIJkLAfh4OgpXpjUDDbvwIDAQABAoIBAEu2ctFVyk/pnbi0
|
|
uRR4rl+hBvKQUeJNGj2ELvL4Ggs5nIAX2IOEZ7JKLC6FqpSrFq7pEd5g57aSvixX
|
|
s3DH4CN7w7fj1ShBCNPlHgIWewdRGpeA74vrDWdwNAEsFdDE6aZeCTOhpDGy1vNJ
|
|
OrtpzS5i9pN0jTvvEneEjtWSZIHiiVlN+0hsFaiwZ6KXON+sDccZPmnP6Fzwj5Rc
|
|
WS0dKSwnxnx0otWgwWFs8nr306nSeMsNmQkHsS9lz4DEVpp9owdzrX1JmbQvNYAV
|
|
ohmB3ET4JYFgerqPXJfed9poueGuWCP6MYhsjNeHN35QhofxdO5/0i3JlZfqwZei
|
|
tNq/0oECgYEA6SqjRqDiIp3ajwyB7Wf0cIQG/P6JZDyN1jl//htgniliIH5UP1Tm
|
|
uAMG5MincV6X9lOyXyh6Yofu5+NR0yt9SqbDZVJ3ZCxKTun7pxJvQFd7wl5bMkiJ
|
|
qVfS08k6gQHHDoO+eel+DtpIfWc+e3tvX0aihSU0GZEMqDXYkkphLGECgYEAxDxb
|
|
+JwJ3N5UEjjkuvFBpuJnmjIaN9HvQkTv3inlx1gLE4iWBZXXsu4aWF8MCUeAAZyP
|
|
42hQDSkCYX/A22tYCEn/jfrU6A+6rkWBTjdUlYLvlSkhosSnO+117WEItb5cUE95
|
|
hF4UY7LNs1AsDkV4WE87f/EjpxSwUAjB2Lfd/B8CgYAJ/JiHsuZcozQ0Qk3iVDyF
|
|
ATKnbWOHFozgqw/PW27U92LLj32eRM2o/gAylmGNmoaZt1YBe2NaiwXxiqv7hnZU
|
|
VzYxRcn1UWxRWvY7Xq/DKrwTRCVVzwOObEOMbKcD1YaoGX50DEso6bKHJH/pnAzW
|
|
INlfKIvFuI+5OK0w/tyQoQKBgQCf/jpaOxaLfrV62eobRQJrByLDBGB97GsvU7di
|
|
IjTWz8DQH0d5rE7d8uWF8ZCFrEcAiV6DYZQK9smbJqbd/uoacAKtBro5rkFdPwwK
|
|
8m/DKqsdqRhkdgOHh7bjYH7Sdy8ax4Fi27WyB6FQtmgFBrz0+zyetsODwQlzZ4Bs
|
|
qpSRrwKBgQC0vWHrY5aGIdF+b8EpP0/SSLLALpMySHyWhDyxYcPqdhszYbjDcavv
|
|
xrrLXNUD2duBHKPVYE+7uVoDkpZXLUQ4x8argo/IwQM6Kh2ma1y83TYMT6XhL1+B
|
|
5UPcl6RXZBCkiU7nFIG6/0XKFqVWc3fU8e09X+iJwXIJ5Jatywtg+g==
|
|
-----END RSA PRIVATE KEY-----
|
|
`
|
|
|
|
func TestCertificateBasedAuth(t *testing.T) {
|
|
signer, err := ssh.ParsePrivateKey([]byte(SERVER_PEM))
|
|
if err != nil {
|
|
t.Fatalf("unable to parse private key: %v", err)
|
|
}
|
|
address, sclose := newMockLineServer(t, signer, CLIENT_CERT_SIGNED_BY_SERVER)
|
|
defer sclose()
|
|
host, p, _ := net.SplitHostPort(address)
|
|
port, _ := strconv.Atoi(p)
|
|
|
|
connInfo := &connectionInfo{
|
|
User: "user",
|
|
Host: host,
|
|
PrivateKey: CLIENT_PEM,
|
|
Certificate: CLIENT_CERT_SIGNED_BY_SERVER,
|
|
Port: uint16(port),
|
|
Timeout: "30s",
|
|
}
|
|
|
|
cfg, err := prepareSSHConfig(connInfo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := &Communicator{
|
|
connInfo: connInfo,
|
|
config: cfg,
|
|
}
|
|
|
|
var cmd remote.Cmd
|
|
stdout := new(bytes.Buffer)
|
|
cmd.Command = "echo foo"
|
|
cmd.Stdout = stdout
|
|
|
|
if err := c.Start(&cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.Disconnect(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestAccUploadFile(t *testing.T) {
|
|
// use the local ssh server and scp binary to check uploads
|
|
if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" {
|
|
t.Log("Skipping Upload Acceptance without SSH_UPLOAD_TEST set")
|
|
t.Skip()
|
|
}
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"user": cty.StringVal(os.Getenv("USER")),
|
|
"host": cty.StringVal("127.0.0.1"),
|
|
"port": cty.StringVal("22"),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
source, err := os.CreateTemp(tmpDir, "tempfile.in")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
content := "this is the file content"
|
|
if _, err := source.WriteString(content); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := source.Seek(0, io.SeekStart); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tmpFile := filepath.Join(tmpDir, "tempFile.out")
|
|
|
|
testUploadSizeHook = func(size int64) {
|
|
if size != int64(len(content)) {
|
|
t.Errorf("expected %d bytes, got %d\n", len(content), size)
|
|
}
|
|
}
|
|
defer func() {
|
|
testUploadSizeHook = nil
|
|
}()
|
|
|
|
err = c.Upload(tmpFile, source)
|
|
if err != nil {
|
|
t.Fatalf("error uploading file: %s", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(tmpFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if string(data) != content {
|
|
t.Fatalf("bad: %s", data)
|
|
}
|
|
}
|
|
|
|
func TestAccHugeUploadFile(t *testing.T) {
|
|
// use the local ssh server and scp binary to check uploads
|
|
if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" {
|
|
t.Log("Skipping Upload Acceptance without SSH_UPLOAD_TEST set")
|
|
t.Skip()
|
|
}
|
|
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"host": cty.StringVal("127.0.0.1"),
|
|
"user": cty.StringVal(os.Getenv("USER")),
|
|
"port": cty.StringVal("22"),
|
|
"timeout": cty.StringVal("30s"),
|
|
})
|
|
|
|
c, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("error creating communicator: %s", err)
|
|
}
|
|
|
|
// copy 4GB of data, random to prevent compression.
|
|
size := int64(1 << 32)
|
|
source := io.LimitReader(rand.New(rand.NewSource(0)), size)
|
|
|
|
dest, err := os.CreateTemp("", "communicator")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
destName := dest.Name()
|
|
dest.Close()
|
|
defer os.Remove(destName)
|
|
|
|
t.Log("Uploading to", destName)
|
|
|
|
// bypass the Upload method so we can directly supply the file size
|
|
// preventing the extra copy of the huge file.
|
|
targetDir := filepath.Dir(destName)
|
|
targetFile := filepath.Base(destName)
|
|
|
|
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
|
|
return scpUploadFile(targetFile, source, w, stdoutR, size)
|
|
}
|
|
|
|
cmd, err := quoteShell([]string{"scp", "-vt", targetDir}, c.connInfo.TargetPlatform)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = c.scpSession(cmd, scpFunc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// check the final file size
|
|
fs, err := os.Stat(destName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if fs.Size() != size {
|
|
t.Fatalf("expected file size of %d, got %d", size, fs.Size())
|
|
}
|
|
}
|
|
|
|
func TestScriptPath(t *testing.T) {
|
|
cases := []struct {
|
|
Input string
|
|
Pattern string
|
|
}{
|
|
{
|
|
"/tmp/script.sh",
|
|
`^/tmp/script\.sh$`,
|
|
},
|
|
{
|
|
"/tmp/script_%RAND%.sh",
|
|
`^/tmp/script_(\d+)\.sh$`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"host": cty.StringVal("127.0.0.1"),
|
|
"script_path": cty.StringVal(tc.Input),
|
|
})
|
|
|
|
comm, err := New(v)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
output := comm.ScriptPath()
|
|
|
|
match, err := regexp.Match(tc.Pattern, []byte(output))
|
|
if err != nil {
|
|
t.Fatalf("bad: %s\n\nerr: %s", tc.Input, err)
|
|
}
|
|
if !match {
|
|
t.Fatalf("bad: %s\n\n%s", tc.Input, output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScriptPath_randSeed(t *testing.T) {
|
|
// Pre GH-4186 fix, this value was the deterministic start the pseudorandom
|
|
// chain of unseeded math/rand values for Int31().
|
|
staticSeedPath := "/tmp/terraform_1298498081.sh"
|
|
c, err := New(cty.ObjectVal(map[string]cty.Value{
|
|
"type": cty.StringVal("ssh"),
|
|
"host": cty.StringVal("127.0.0.1"),
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
path := c.ScriptPath()
|
|
if path == staticSeedPath {
|
|
t.Fatalf("rand not seeded! got: %s", path)
|
|
}
|
|
}
|
|
|
|
var testClientPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDE6A1c4n+OtEPEFlNKTZf2i03L3NylSYmvmJ8OLmzLuPZmJBJt4G3VZ/60s1aKzwLKrTq20S+ONG4zvnK5zIPoauoNNdUJKbg944hB4OE+HDbrBhk7SH+YWCsCILBoSXwAVdUEic6FWf/SeqBSmTBySHvpuNOw16J+SK6Ardx8k64F2tRkZuC6AmOZijgKa/sQKjWAIVPk34ECM6OLfPc3kKUEfkdpYLvuMfuRMfSTlxn5lFC0b0SovK9aWfNMBH9iXLQkieQ5rXoyzUC7mwgnASgl8cqw1UrToiUuhvneduXBhbQfmC/Upv+tL6dSSk+0DlgVKEHuJmc8s8+/qpdL`
|
|
|
|
func acceptUserPass(goodUser, goodPass string) func(ssh.ConnMetadata, []byte) (*ssh.Permissions, error) {
|
|
return func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
if c.User() == goodUser && string(pass) == goodPass {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("password rejected for %q", c.User())
|
|
}
|
|
}
|
|
|
|
func acceptPublicKey(keystr string) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) {
|
|
return func(_ ssh.ConnMetadata, inkey ssh.PublicKey) (*ssh.Permissions, error) {
|
|
goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing key: %w", err)
|
|
}
|
|
|
|
if bytes.Equal(inkey.Marshal(), goodkey.Marshal()) {
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("public key rejected")
|
|
}
|
|
}
|