Files
opentf/internal/communicator/ssh/communicator_test.go
Martin Atkins 068df07d11 various: Remove legacy "+build" comments
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>
2025-11-01 08:00:01 -03:00

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")
}
}