github.com/opentofu/opentofu@v1.7.1/internal/communicator/ssh/communicator_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 //go:build !race 7 // +build !race 8 9 package ssh 10 11 import ( 12 "bufio" 13 "bytes" 14 "encoding/base64" 15 "fmt" 16 "io" 17 "math/rand" 18 "net" 19 "os" 20 "path/filepath" 21 "regexp" 22 "strconv" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/opentofu/opentofu/internal/communicator/remote" 28 "github.com/zclconf/go-cty/cty" 29 "golang.org/x/crypto/ssh" 30 ) 31 32 // private key for mock server 33 const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY----- 34 MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU 35 70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx 36 9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF 37 tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z 38 s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc 39 qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT 40 +IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea 41 riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH 42 D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh 43 atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT 44 b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN 45 ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M 46 MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4 47 KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8 48 e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1 49 D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+ 50 3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj 51 orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw 52 64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc 53 XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc 54 QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g 55 /SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ 56 I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk 57 gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl 58 NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw== 59 -----END RSA PRIVATE KEY-----` 60 61 // this cert was signed by the key from testCAPublicKey 62 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=` 63 64 const testCAPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrozyZIhdEvalCn+eSzHH94cO9ykiywA13ntWI7mJcHBwYTeCYWG8E9zGXyp2iDOjCGudM0Tdt8o0OofKChk9Z/qiUN0G8y1kmaXBlBM3qA5R9NPpvMYMNkYLfX6ivtZCnqrsbzaoqN2Oc/7H2StHzJWh/XCGu9otQZA6vdv1oSmAsZOjw/xIGaGQqDUaLq21J280PP1qSbdJHf76iSHE+TWe3YpqV946JWM5tCh0DykZ10VznvxYpUjzhr07IN3tVKxOXbPnnU7lX6IaLIWgfzLqwSyheeux05c3JLF9iF4sFu8ou4hwQz1iuUTU1jxgwZP0w/bkXgFFs0949lW81` 65 66 func newMockLineServer(t *testing.T, signer ssh.Signer, pubKey string) string { 67 serverConfig := &ssh.ServerConfig{ 68 PasswordCallback: acceptUserPass("user", "pass"), 69 PublicKeyCallback: acceptPublicKey(pubKey), 70 } 71 72 var err error 73 if signer == nil { 74 signer, err = ssh.ParsePrivateKey([]byte(testServerPrivateKey)) 75 if err != nil { 76 t.Fatalf("unable to parse private key: %s", err) 77 } 78 } 79 serverConfig.AddHostKey(signer) 80 81 l, err := net.Listen("tcp", "127.0.0.1:0") 82 if err != nil { 83 t.Fatalf("Unable to listen for connection: %s", err) 84 } 85 86 go func() { 87 defer l.Close() 88 c, err := l.Accept() 89 if err != nil { 90 t.Errorf("Unable to accept incoming connection: %s", err) 91 } 92 defer c.Close() 93 conn, chans, _, err := ssh.NewServerConn(c, serverConfig) 94 if err != nil { 95 t.Logf("Handshaking error: %v", err) 96 } 97 t.Log("Accepted SSH connection") 98 99 for newChannel := range chans { 100 channel, requests, err := newChannel.Accept() 101 if err != nil { 102 t.Errorf("Unable to accept channel.") 103 } 104 t.Log("Accepted channel") 105 106 go func(in <-chan *ssh.Request) { 107 defer channel.Close() 108 for req := range in { 109 // since this channel's requests are serviced serially, 110 // this will block keepalive probes, and can simulate a 111 // hung connection. 112 if bytes.Contains(req.Payload, []byte("sleep")) { 113 time.Sleep(time.Second) 114 } 115 116 if req.WantReply { 117 req.Reply(true, nil) 118 } 119 } 120 }(requests) 121 } 122 conn.Close() 123 }() 124 125 return l.Addr().String() 126 } 127 128 func TestNew_Invalid(t *testing.T) { 129 address := newMockLineServer(t, nil, testClientPublicKey) 130 parts := strings.Split(address, ":") 131 132 v := cty.ObjectVal(map[string]cty.Value{ 133 "type": cty.StringVal("ssh"), 134 "user": cty.StringVal("user"), 135 "password": cty.StringVal("i-am-invalid"), 136 "host": cty.StringVal(parts[0]), 137 "port": cty.StringVal(parts[1]), 138 "timeout": cty.StringVal("30s"), 139 }) 140 141 c, err := New(v) 142 if err != nil { 143 t.Fatalf("error creating communicator: %s", err) 144 } 145 146 err = c.Connect(nil) 147 if err == nil { 148 t.Fatal("should have had an error connecting") 149 } 150 } 151 152 func TestNew_InvalidHost(t *testing.T) { 153 v := cty.ObjectVal(map[string]cty.Value{ 154 "type": cty.StringVal("ssh"), 155 "user": cty.StringVal("user"), 156 "password": cty.StringVal("i-am-invalid"), 157 "port": cty.StringVal("22"), 158 "timeout": cty.StringVal("30s"), 159 }) 160 161 _, err := New(v) 162 if err == nil { 163 t.Fatal("should have had an error creating communicator") 164 } 165 } 166 167 func TestStart(t *testing.T) { 168 address := newMockLineServer(t, nil, testClientPublicKey) 169 parts := strings.Split(address, ":") 170 171 v := cty.ObjectVal(map[string]cty.Value{ 172 "type": cty.StringVal("ssh"), 173 "user": cty.StringVal("user"), 174 "password": cty.StringVal("pass"), 175 "host": cty.StringVal(parts[0]), 176 "port": cty.StringVal(parts[1]), 177 "timeout": cty.StringVal("30s"), 178 }) 179 180 c, err := New(v) 181 if err != nil { 182 t.Fatalf("error creating communicator: %s", err) 183 } 184 185 var cmd remote.Cmd 186 stdout := new(bytes.Buffer) 187 cmd.Command = "echo foo" 188 cmd.Stdout = stdout 189 190 err = c.Start(&cmd) 191 if err != nil { 192 t.Fatalf("error executing remote command: %s", err) 193 } 194 } 195 196 // TestKeepAlives verifies that the keepalive messages don't interfere with 197 // normal operation of the client. 198 func TestKeepAlives(t *testing.T) { 199 ivl := keepAliveInterval 200 keepAliveInterval = 250 * time.Millisecond 201 defer func() { keepAliveInterval = ivl }() 202 203 address := newMockLineServer(t, nil, testClientPublicKey) 204 parts := strings.Split(address, ":") 205 206 v := cty.ObjectVal(map[string]cty.Value{ 207 "type": cty.StringVal("ssh"), 208 "user": cty.StringVal("user"), 209 "password": cty.StringVal("pass"), 210 "host": cty.StringVal(parts[0]), 211 "port": cty.StringVal(parts[1]), 212 }) 213 214 c, err := New(v) 215 if err != nil { 216 t.Fatalf("error creating communicator: %s", err) 217 } 218 219 if err := c.Connect(nil); err != nil { 220 t.Fatal(err) 221 } 222 223 var cmd remote.Cmd 224 stdout := new(bytes.Buffer) 225 cmd.Command = "sleep" 226 cmd.Stdout = stdout 227 228 // wait a bit before executing the command, so that at least 1 keepalive is sent 229 time.Sleep(500 * time.Millisecond) 230 231 err = c.Start(&cmd) 232 if err != nil { 233 t.Fatalf("error executing remote command: %s", err) 234 } 235 } 236 237 // TestDeadConnection verifies that failed keepalive messages will eventually 238 // kill the connection. 239 func TestFailedKeepAlives(t *testing.T) { 240 ivl := keepAliveInterval 241 del := maxKeepAliveDelay 242 maxKeepAliveDelay = 500 * time.Millisecond 243 keepAliveInterval = 250 * time.Millisecond 244 defer func() { 245 keepAliveInterval = ivl 246 maxKeepAliveDelay = del 247 }() 248 249 address := newMockLineServer(t, nil, testClientPublicKey) 250 parts := strings.Split(address, ":") 251 252 v := cty.ObjectVal(map[string]cty.Value{ 253 "type": cty.StringVal("ssh"), 254 "user": cty.StringVal("user"), 255 "password": cty.StringVal("pass"), 256 "host": cty.StringVal(parts[0]), 257 "port": cty.StringVal(parts[1]), 258 "timeout": cty.StringVal("30s"), 259 }) 260 261 c, err := New(v) 262 if err != nil { 263 t.Fatalf("error creating communicator: %s", err) 264 } 265 266 if err := c.Connect(nil); err != nil { 267 t.Fatal(err) 268 } 269 var cmd remote.Cmd 270 stdout := new(bytes.Buffer) 271 cmd.Command = "sleep" 272 cmd.Stdout = stdout 273 274 err = c.Start(&cmd) 275 if err == nil { 276 t.Fatal("expected connection error") 277 } 278 } 279 280 func TestLostConnection(t *testing.T) { 281 address := newMockLineServer(t, nil, testClientPublicKey) 282 parts := strings.Split(address, ":") 283 284 v := cty.ObjectVal(map[string]cty.Value{ 285 "type": cty.StringVal("ssh"), 286 "user": cty.StringVal("user"), 287 "password": cty.StringVal("pass"), 288 "host": cty.StringVal(parts[0]), 289 "port": cty.StringVal(parts[1]), 290 "timeout": cty.StringVal("30s"), 291 }) 292 293 c, err := New(v) 294 if err != nil { 295 t.Fatalf("error creating communicator: %s", err) 296 } 297 298 var cmd remote.Cmd 299 stdout := new(bytes.Buffer) 300 cmd.Command = "echo foo" 301 cmd.Stdout = stdout 302 303 err = c.Start(&cmd) 304 if err != nil { 305 t.Fatalf("error executing remote command: %s", err) 306 } 307 308 // The test server can't execute anything, so Wait will block, unless 309 // there's an error. Disconnect the communicator transport, to cause the 310 // command to fail. 311 go func() { 312 time.Sleep(100 * time.Millisecond) 313 c.Disconnect() 314 }() 315 316 err = cmd.Wait() 317 if err == nil { 318 t.Fatal("expected communicator error") 319 } 320 } 321 322 func TestHostKey(t *testing.T) { 323 // get the server's public key 324 signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey)) 325 if err != nil { 326 t.Fatalf("unable to parse private key: %v", err) 327 } 328 pubKey := fmt.Sprintf("ssh-rsa %s", base64.StdEncoding.EncodeToString(signer.PublicKey().Marshal())) 329 330 address := newMockLineServer(t, nil, testClientPublicKey) 331 host, p, _ := net.SplitHostPort(address) 332 port, _ := strconv.Atoi(p) 333 334 connInfo := &connectionInfo{ 335 User: "user", 336 Password: "pass", 337 Host: host, 338 HostKey: pubKey, 339 Port: uint16(port), 340 Timeout: "30s", 341 } 342 343 cfg, err := prepareSSHConfig(connInfo) 344 if err != nil { 345 t.Fatal(err) 346 } 347 348 c := &Communicator{ 349 connInfo: connInfo, 350 config: cfg, 351 } 352 353 var cmd remote.Cmd 354 stdout := new(bytes.Buffer) 355 cmd.Command = "echo foo" 356 cmd.Stdout = stdout 357 358 if err := c.Start(&cmd); err != nil { 359 t.Fatal(err) 360 } 361 if err := c.Disconnect(); err != nil { 362 t.Fatal(err) 363 } 364 365 // now check with the wrong HostKey 366 address = newMockLineServer(t, nil, testClientPublicKey) 367 _, p, _ = net.SplitHostPort(address) 368 port, _ = strconv.Atoi(p) 369 370 connInfo.HostKey = testClientPublicKey 371 connInfo.Port = uint16(port) 372 373 cfg, err = prepareSSHConfig(connInfo) 374 if err != nil { 375 t.Fatal(err) 376 } 377 378 c = &Communicator{ 379 connInfo: connInfo, 380 config: cfg, 381 } 382 383 err = c.Start(&cmd) 384 if err == nil || !strings.Contains(err.Error(), "mismatch") { 385 t.Fatalf("expected host key mismatch, got error:%v", err) 386 } 387 } 388 389 func TestHostCert(t *testing.T) { 390 pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(testServerHostCert)) 391 if err != nil { 392 t.Fatal(err) 393 } 394 395 signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey)) 396 if err != nil { 397 t.Fatal(err) 398 } 399 400 signer, err = ssh.NewCertSigner(pk.(*ssh.Certificate), signer) 401 if err != nil { 402 t.Fatal(err) 403 } 404 405 address := newMockLineServer(t, signer, testClientPublicKey) 406 host, p, _ := net.SplitHostPort(address) 407 port, _ := strconv.Atoi(p) 408 409 connInfo := &connectionInfo{ 410 User: "user", 411 Password: "pass", 412 Host: host, 413 HostKey: testCAPublicKey, 414 Port: uint16(port), 415 Timeout: "30s", 416 } 417 418 cfg, err := prepareSSHConfig(connInfo) 419 if err != nil { 420 t.Fatal(err) 421 } 422 423 c := &Communicator{ 424 connInfo: connInfo, 425 config: cfg, 426 } 427 428 var cmd remote.Cmd 429 stdout := new(bytes.Buffer) 430 cmd.Command = "echo foo" 431 cmd.Stdout = stdout 432 433 if err := c.Start(&cmd); err != nil { 434 t.Fatal(err) 435 } 436 if err := c.Disconnect(); err != nil { 437 t.Fatal(err) 438 } 439 440 // now check with the wrong HostKey 441 address = newMockLineServer(t, signer, testClientPublicKey) 442 _, p, _ = net.SplitHostPort(address) 443 port, _ = strconv.Atoi(p) 444 445 connInfo.HostKey = testClientPublicKey 446 connInfo.Port = uint16(port) 447 448 cfg, err = prepareSSHConfig(connInfo) 449 if err != nil { 450 t.Fatal(err) 451 } 452 453 c = &Communicator{ 454 connInfo: connInfo, 455 config: cfg, 456 } 457 458 err = c.Start(&cmd) 459 if err == nil || !strings.Contains(err.Error(), "authorities") { 460 t.Fatalf("expected host key mismatch, got error:%v", err) 461 } 462 } 463 464 const SERVER_PEM = `-----BEGIN RSA PRIVATE KEY----- 465 MIIEpAIBAAKCAQEA8CkDr7uxCFt6lQUVwS8NyPO+fQNxORoGnMnN/XhVJZvpqyKR 466 Uji9R0d8D66bYxUUsabXjP2y4HTVzbZtnvXFZZshk0cOtJjjekpYJaLK2esPR/iX 467 wvSltNkrDQDPN/RmgEEMIevW8AgrPsqrnybFHxTpd7rEUHXBOe4nMNRIg3XHykB6 468 jZk8q5bBPUe3I/f0DK5TJEBpTc6dO3P/j93u55VUqr39/SPRHnld2mCw+c8v6UOh 469 sssO/DIZFPScD3DYqsk2N+/nz9zXfcOTdWGhawgxuIo1DTokrNQbG3pDrLqcWgqj 470 13vqJFCmRA0O2CQIwJePd6+Np/XO3Uh/KL6FlQIDAQABAoIBAQCmvQMXNmvCDqk7 471 30zsVDvw4fHGH+azK3Od1aqTqcEMHISOUbCtckFPxLzIsoSltRQqB1kuRVG07skm 472 Stsu+xny4lLcSwBVuLRuykEK2EyYIc/5Owo6y9pkhkaSf5ZfFes4bnD6+B/BhRpp 473 PRMMq0E+xCkX/G6iIi9mhgdlqm0x/vKtjzQeeshw9+gRcRLUpX+UeKFKXMXcDayx 474 qekr1bAaQKNBhTK+CbZjcqzG4f+BXVGRTZ9nsPAV+yTnWUCU0TghwPmtthHbebqa 475 9hlkum7qik/bQj/tjJ8/b0vTfHQSVxhtPG/ZV2Tn9ZuL/vrkYqeyMU8XkJ/uaEvH 476 WPyOcB4BAoGBAP5o5JSEtPog+U3JFrLNSRjz5ofZNVkJzice+0XyqlzJDHhX5tF8 477 mriYQZLLXYhckBm4IdkhTn/dVbXNQTzyy2WVuO5nU8bkCMvGL9CGpW4YGqwGf7NX 478 e4H3emtRjLv8VZpUHe/RUUDhmYvMSt1qmXuskfpROuGfLhQBUd6A4J+BAoGBAPGp 479 UcMKjrxZ5qjYU6DLgS+xeca4Eu70HgdbSQbRo45WubXjyXvTRFij36DrpxJWf1D7 480 lIsyBifoTra/lAuC1NQXGYWjTCdk2ey8Ll5qOgiXvE6lINHABr+U/Z90/g6LuML2 481 VzaZbq/QLcT3yVsdyTogKckzCaKsCpusyHE1CXAVAoGAd6kMglKc8N0bhZukgnsN 482 +5+UeacPcY6sGTh4RWErAjNKGzx1A2lROKvcg9gFaULoQECcIw2IZ5nKW5VsLueg 483 BWrTrcaJ4A2XmYjhKnp6SvspaGoyHD90hx/Iw7t6r1yzQsB3yDmytwqldtyjBdvC 484 zynPC2azhDWjraMlR7tka4ECgYAxwvLiHa9sm3qCtCDsUFtmrb3srITBjaUNUL/F 485 1q8+JR+Sk7gudj9xnTT0VvINNaB71YIt83wPBagHu4VJpYQbtDH+MbUBu6OgOtO1 486 f1w53rzY2OncJxV8p7pd9mJGLoE6LC2jQY7oRw7Vq0xcJdME1BCmrIrEY3a/vaF8 487 pjYuTQKBgQCIOH23Xita8KmhH0NdlWxZfcQt1j3AnOcKe6UyN4BsF8hqS7eTA52s 488 WjG5X2IBl7gs1eMM1qkqR8npS9nwfO/pBmZPwjiZoilypXxWj+c+P3vwre2yija4 489 bXgFVj4KFBwhr1+8KcobxC0SAPEouMvSkxzjjw+gnebozUtPlud9jA== 490 -----END RSA PRIVATE KEY----- 491 ` 492 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` 493 const CLIENT_PEM = `-----BEGIN RSA PRIVATE KEY----- 494 MIIEpAIBAAKCAQEAsruexO8yFkchFrmojoopDkmShY28C5KRPX26BXyy7DVCY+M8 495 PuW2VCbc1ivK6D44C/LNaD53K6OcLk7SNjbuH5KXhStB2996OhMpLLo+DpuPulHp 496 p9UBHG9G2aivGcoPQ4PXXj+y3hNL+/G+f42WnxYMMb/v7DITND0Q9+DQg2HGJpJB 497 5b0LUFhQHWz1woIITm3nWcmHaKmVboU7IjzFrUEv3HPTOLb+pjzrkJYWbd0fzof5 498 MFMruOaBkW+W4TuCYnKMxUt6HgpaVPlDG4LJZMwKSasWQVje/jwxUmYSpNhrJcns 499 j+LB9xN9GdNMlYv5UGIJkLAfh4OgpXpjUDDbvwIDAQABAoIBAEu2ctFVyk/pnbi0 500 uRR4rl+hBvKQUeJNGj2ELvL4Ggs5nIAX2IOEZ7JKLC6FqpSrFq7pEd5g57aSvixX 501 s3DH4CN7w7fj1ShBCNPlHgIWewdRGpeA74vrDWdwNAEsFdDE6aZeCTOhpDGy1vNJ 502 OrtpzS5i9pN0jTvvEneEjtWSZIHiiVlN+0hsFaiwZ6KXON+sDccZPmnP6Fzwj5Rc 503 WS0dKSwnxnx0otWgwWFs8nr306nSeMsNmQkHsS9lz4DEVpp9owdzrX1JmbQvNYAV 504 ohmB3ET4JYFgerqPXJfed9poueGuWCP6MYhsjNeHN35QhofxdO5/0i3JlZfqwZei 505 tNq/0oECgYEA6SqjRqDiIp3ajwyB7Wf0cIQG/P6JZDyN1jl//htgniliIH5UP1Tm 506 uAMG5MincV6X9lOyXyh6Yofu5+NR0yt9SqbDZVJ3ZCxKTun7pxJvQFd7wl5bMkiJ 507 qVfS08k6gQHHDoO+eel+DtpIfWc+e3tvX0aihSU0GZEMqDXYkkphLGECgYEAxDxb 508 +JwJ3N5UEjjkuvFBpuJnmjIaN9HvQkTv3inlx1gLE4iWBZXXsu4aWF8MCUeAAZyP 509 42hQDSkCYX/A22tYCEn/jfrU6A+6rkWBTjdUlYLvlSkhosSnO+117WEItb5cUE95 510 hF4UY7LNs1AsDkV4WE87f/EjpxSwUAjB2Lfd/B8CgYAJ/JiHsuZcozQ0Qk3iVDyF 511 ATKnbWOHFozgqw/PW27U92LLj32eRM2o/gAylmGNmoaZt1YBe2NaiwXxiqv7hnZU 512 VzYxRcn1UWxRWvY7Xq/DKrwTRCVVzwOObEOMbKcD1YaoGX50DEso6bKHJH/pnAzW 513 INlfKIvFuI+5OK0w/tyQoQKBgQCf/jpaOxaLfrV62eobRQJrByLDBGB97GsvU7di 514 IjTWz8DQH0d5rE7d8uWF8ZCFrEcAiV6DYZQK9smbJqbd/uoacAKtBro5rkFdPwwK 515 8m/DKqsdqRhkdgOHh7bjYH7Sdy8ax4Fi27WyB6FQtmgFBrz0+zyetsODwQlzZ4Bs 516 qpSRrwKBgQC0vWHrY5aGIdF+b8EpP0/SSLLALpMySHyWhDyxYcPqdhszYbjDcavv 517 xrrLXNUD2duBHKPVYE+7uVoDkpZXLUQ4x8argo/IwQM6Kh2ma1y83TYMT6XhL1+B 518 5UPcl6RXZBCkiU7nFIG6/0XKFqVWc3fU8e09X+iJwXIJ5Jatywtg+g== 519 -----END RSA PRIVATE KEY----- 520 ` 521 522 func TestCertificateBasedAuth(t *testing.T) { 523 signer, err := ssh.ParsePrivateKey([]byte(SERVER_PEM)) 524 if err != nil { 525 t.Fatalf("unable to parse private key: %v", err) 526 } 527 address := newMockLineServer(t, signer, CLIENT_CERT_SIGNED_BY_SERVER) 528 host, p, _ := net.SplitHostPort(address) 529 port, _ := strconv.Atoi(p) 530 531 connInfo := &connectionInfo{ 532 User: "user", 533 Host: host, 534 PrivateKey: CLIENT_PEM, 535 Certificate: CLIENT_CERT_SIGNED_BY_SERVER, 536 Port: uint16(port), 537 Timeout: "30s", 538 } 539 540 cfg, err := prepareSSHConfig(connInfo) 541 if err != nil { 542 t.Fatal(err) 543 } 544 545 c := &Communicator{ 546 connInfo: connInfo, 547 config: cfg, 548 } 549 550 var cmd remote.Cmd 551 stdout := new(bytes.Buffer) 552 cmd.Command = "echo foo" 553 cmd.Stdout = stdout 554 555 if err := c.Start(&cmd); err != nil { 556 t.Fatal(err) 557 } 558 if err := c.Disconnect(); err != nil { 559 t.Fatal(err) 560 } 561 } 562 563 func TestAccUploadFile(t *testing.T) { 564 // use the local ssh server and scp binary to check uploads 565 if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" { 566 t.Log("Skipping Upload Acceptance without SSH_UPLOAD_TEST set") 567 t.Skip() 568 } 569 570 v := cty.ObjectVal(map[string]cty.Value{ 571 "type": cty.StringVal("ssh"), 572 "user": cty.StringVal(os.Getenv("USER")), 573 "host": cty.StringVal("127.0.0.1"), 574 "port": cty.StringVal("22"), 575 "timeout": cty.StringVal("30s"), 576 }) 577 578 c, err := New(v) 579 if err != nil { 580 t.Fatalf("error creating communicator: %s", err) 581 } 582 583 tmpDir := t.TempDir() 584 source, err := os.CreateTemp(tmpDir, "tempfile.in") 585 if err != nil { 586 t.Fatal(err) 587 } 588 589 content := "this is the file content" 590 if _, err := source.WriteString(content); err != nil { 591 t.Fatal(err) 592 } 593 source.Seek(0, io.SeekStart) 594 595 tmpFile := filepath.Join(tmpDir, "tempFile.out") 596 597 testUploadSizeHook = func(size int64) { 598 if size != int64(len(content)) { 599 t.Errorf("expected %d bytes, got %d\n", len(content), size) 600 } 601 } 602 defer func() { 603 testUploadSizeHook = nil 604 }() 605 606 err = c.Upload(tmpFile, source) 607 if err != nil { 608 t.Fatalf("error uploading file: %s", err) 609 } 610 611 data, err := os.ReadFile(tmpFile) 612 if err != nil { 613 t.Fatal(err) 614 } 615 616 if string(data) != content { 617 t.Fatalf("bad: %s", data) 618 } 619 } 620 621 func TestAccHugeUploadFile(t *testing.T) { 622 // use the local ssh server and scp binary to check uploads 623 if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" { 624 t.Log("Skipping Upload Acceptance without SSH_UPLOAD_TEST set") 625 t.Skip() 626 } 627 628 v := cty.ObjectVal(map[string]cty.Value{ 629 "type": cty.StringVal("ssh"), 630 "host": cty.StringVal("127.0.0.1"), 631 "user": cty.StringVal(os.Getenv("USER")), 632 "port": cty.StringVal("22"), 633 "timeout": cty.StringVal("30s"), 634 }) 635 636 c, err := New(v) 637 if err != nil { 638 t.Fatalf("error creating communicator: %s", err) 639 } 640 641 // copy 4GB of data, random to prevent compression. 642 size := int64(1 << 32) 643 source := io.LimitReader(rand.New(rand.NewSource(0)), size) 644 645 dest, err := os.CreateTemp("", "communicator") 646 if err != nil { 647 t.Fatal(err) 648 } 649 destName := dest.Name() 650 dest.Close() 651 defer os.Remove(destName) 652 653 t.Log("Uploading to", destName) 654 655 // bypass the Upload method so we can directly supply the file size 656 // preventing the extra copy of the huge file. 657 targetDir := filepath.Dir(destName) 658 targetFile := filepath.Base(destName) 659 660 scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error { 661 return scpUploadFile(targetFile, source, w, stdoutR, size) 662 } 663 664 cmd, err := quoteShell([]string{"scp", "-vt", targetDir}, c.connInfo.TargetPlatform) 665 if err != nil { 666 t.Fatal(err) 667 } 668 err = c.scpSession(cmd, scpFunc) 669 if err != nil { 670 t.Fatal(err) 671 } 672 673 // check the final file size 674 fs, err := os.Stat(destName) 675 if err != nil { 676 t.Fatal(err) 677 } 678 679 if fs.Size() != size { 680 t.Fatalf("expected file size of %d, got %d", size, fs.Size()) 681 } 682 } 683 684 func TestScriptPath(t *testing.T) { 685 cases := []struct { 686 Input string 687 Pattern string 688 }{ 689 { 690 "/tmp/script.sh", 691 `^/tmp/script\.sh$`, 692 }, 693 { 694 "/tmp/script_%RAND%.sh", 695 `^/tmp/script_(\d+)\.sh$`, 696 }, 697 } 698 699 for _, tc := range cases { 700 v := cty.ObjectVal(map[string]cty.Value{ 701 "type": cty.StringVal("ssh"), 702 "host": cty.StringVal("127.0.0.1"), 703 "script_path": cty.StringVal(tc.Input), 704 }) 705 706 comm, err := New(v) 707 if err != nil { 708 t.Fatalf("err: %s", err) 709 } 710 output := comm.ScriptPath() 711 712 match, err := regexp.Match(tc.Pattern, []byte(output)) 713 if err != nil { 714 t.Fatalf("bad: %s\n\nerr: %s", tc.Input, err) 715 } 716 if !match { 717 t.Fatalf("bad: %s\n\n%s", tc.Input, output) 718 } 719 } 720 } 721 722 func TestScriptPath_randSeed(t *testing.T) { 723 // Pre GH-4186 fix, this value was the deterministic start the pseudorandom 724 // chain of unseeded math/rand values for Int31(). 725 staticSeedPath := "/tmp/terraform_1298498081.sh" 726 c, err := New(cty.ObjectVal(map[string]cty.Value{ 727 "type": cty.StringVal("ssh"), 728 "host": cty.StringVal("127.0.0.1"), 729 })) 730 if err != nil { 731 t.Fatalf("err: %s", err) 732 } 733 path := c.ScriptPath() 734 if path == staticSeedPath { 735 t.Fatalf("rand not seeded! got: %s", path) 736 } 737 } 738 739 var testClientPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDE6A1c4n+OtEPEFlNKTZf2i03L3NylSYmvmJ8OLmzLuPZmJBJt4G3VZ/60s1aKzwLKrTq20S+ONG4zvnK5zIPoauoNNdUJKbg944hB4OE+HDbrBhk7SH+YWCsCILBoSXwAVdUEic6FWf/SeqBSmTBySHvpuNOw16J+SK6Ardx8k64F2tRkZuC6AmOZijgKa/sQKjWAIVPk34ECM6OLfPc3kKUEfkdpYLvuMfuRMfSTlxn5lFC0b0SovK9aWfNMBH9iXLQkieQ5rXoyzUC7mwgnASgl8cqw1UrToiUuhvneduXBhbQfmC/Upv+tL6dSSk+0DlgVKEHuJmc8s8+/qpdL` 740 741 func acceptUserPass(goodUser, goodPass string) func(ssh.ConnMetadata, []byte) (*ssh.Permissions, error) { 742 return func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { 743 if c.User() == goodUser && string(pass) == goodPass { 744 return nil, nil 745 } 746 return nil, fmt.Errorf("password rejected for %q", c.User()) 747 } 748 } 749 750 func acceptPublicKey(keystr string) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) { 751 return func(_ ssh.ConnMetadata, inkey ssh.PublicKey) (*ssh.Permissions, error) { 752 goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr)) 753 if err != nil { 754 return nil, fmt.Errorf("error parsing key: %w", err) 755 } 756 757 if bytes.Equal(inkey.Marshal(), goodkey.Marshal()) { 758 return nil, nil 759 } 760 761 return nil, fmt.Errorf("public key rejected") 762 } 763 }