github.com/candidpartners/terraform@v0.9.5-0.20171005231213-29f5f88820f6/communicator/ssh/communicator_test.go (about) 1 // +build !race 2 3 package ssh 4 5 import ( 6 "bufio" 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "math/rand" 12 "net" 13 "os" 14 "path/filepath" 15 "regexp" 16 "strings" 17 "testing" 18 19 "github.com/hashicorp/terraform/communicator/remote" 20 "github.com/hashicorp/terraform/terraform" 21 "golang.org/x/crypto/ssh" 22 ) 23 24 // private key for mock server 25 const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY----- 26 MIIEpAIBAAKCAQEA19lGVsTqIT5iiNYRgnoY1CwkbETW5cq+Rzk5v/kTlf31XpSU 27 70HVWkbTERECjaYdXM2gGcbb+sxpq6GtXf1M3kVomycqhxwhPv4Cr6Xp4WT/jkFx 28 9z+FFzpeodGJWjOH6L2H5uX1Cvr9EDdQp9t9/J32/qBFntY8GwoUI/y/1MSTmMiF 29 tupdMODN064vd3gyMKTwrlQ8tZM6aYuyOPsutLlUY7M5x5FwMDYvnPDSeyT/Iw0z 30 s3B+NCyqeeMd2T7YzQFnRATj0M7rM5LoSs7DVqVriOEABssFyLj31PboaoLhOKgc 31 qoM9khkNzr7FHVvi+DhYM2jD0DwvqZLN6NmnLwIDAQABAoIBAQCGVj+kuSFOV1lT 32 +IclQYA6bM6uY5mroqcSBNegVxCNhWU03BxlW//BE9tA/+kq53vWylMeN9mpGZea 33 riEMIh25KFGWXqXlOOioH8bkMsqA8S7sBmc7jljyv+0toQ9vCCtJ+sueNPhxQQxH 34 D2YvUjfzBQ04I9+wn30BByDJ1QA/FoPsunxIOUCcRBE/7jxuLYcpR+JvEF68yYIh 35 atXRld4W4in7T65YDR8jK1Uj9XAcNeDYNpT/M6oFLx1aPIlkG86aCWRO19S1jLPT 36 b1ZAKHHxPMCVkSYW0RqvIgLXQOR62D0Zne6/2wtzJkk5UCjkSQ2z7ZzJpMkWgDgN 37 ifCULFPBAoGBAPoMZ5q1w+zB+knXUD33n1J+niN6TZHJulpf2w5zsW+m2K6Zn62M 38 MXndXlVAHtk6p02q9kxHdgov34Uo8VpuNjbS1+abGFTI8NZgFo+bsDxJdItemwC4 39 KJ7L1iz39hRN/ZylMRLz5uTYRGddCkeIHhiG2h7zohH/MaYzUacXEEy3AoGBANz8 40 e/msleB+iXC0cXKwds26N4hyMdAFE5qAqJXvV3S2W8JZnmU+sS7vPAWMYPlERPk1 41 D8Q2eXqdPIkAWBhrx4RxD7rNc5qFNcQWEhCIxC9fccluH1y5g2M+4jpMX2CT8Uv+ 42 3z+NoJ5uDTXZTnLCfoZzgZ4nCZVZ+6iU5U1+YXFJAoGBANLPpIV920n/nJmmquMj 43 orI1R/QXR9Cy56cMC65agezlGOfTYxk5Cfl5Ve+/2IJCfgzwJyjWUsFx7RviEeGw 44 64o7JoUom1HX+5xxdHPsyZ96OoTJ5RqtKKoApnhRMamau0fWydH1yeOEJd+TRHhc 45 XStGfhz8QNa1dVFvENczja1vAoGABGWhsd4VPVpHMc7lUvrf4kgKQtTC2PjA4xoc 46 QJ96hf/642sVE76jl+N6tkGMzGjnVm4P2j+bOy1VvwQavKGoXqJBRd5Apppv727g 47 /SM7hBXKFc/zH80xKBBgP/i1DR7kdjakCoeu4ngeGywvu2jTS6mQsqzkK+yWbUxJ 48 I7mYBsECgYB/KNXlTEpXtz/kwWCHFSYA8U74l7zZbVD8ul0e56JDK+lLcJ0tJffk 49 gqnBycHj6AhEycjda75cs+0zybZvN4x65KZHOGW/O/7OAWEcZP5TPb3zf9ned3Hl 50 NsZoFj52ponUM6+99A2CmezFCN16c4mbA//luWF+k3VVqR6BpkrhKw== 51 -----END RSA PRIVATE KEY-----` 52 53 var serverConfig = &ssh.ServerConfig{ 54 PasswordCallback: acceptUserPass("user", "pass"), 55 PublicKeyCallback: acceptPublicKey(testClientPublicKey), 56 } 57 58 func init() { 59 // Parse and set the private key of the server, required to accept connections 60 signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey)) 61 if err != nil { 62 panic("unable to parse private key: " + err.Error()) 63 } 64 serverConfig.AddHostKey(signer) 65 } 66 67 func newMockLineServer(t *testing.T) string { 68 l, err := net.Listen("tcp", "127.0.0.1:0") 69 if err != nil { 70 t.Fatalf("Unable to listen for connection: %s", err) 71 } 72 73 go func() { 74 defer l.Close() 75 c, err := l.Accept() 76 if err != nil { 77 t.Errorf("Unable to accept incoming connection: %s", err) 78 } 79 defer c.Close() 80 conn, chans, _, err := ssh.NewServerConn(c, serverConfig) 81 if err != nil { 82 t.Logf("Handshaking error: %v", err) 83 } 84 t.Log("Accepted SSH connection") 85 86 for newChannel := range chans { 87 channel, requests, err := newChannel.Accept() 88 if err != nil { 89 t.Errorf("Unable to accept channel.") 90 } 91 t.Log("Accepted channel") 92 93 go func(in <-chan *ssh.Request) { 94 for req := range in { 95 if req.WantReply { 96 req.Reply(true, nil) 97 } 98 } 99 }(requests) 100 101 go func(newChannel ssh.NewChannel) { 102 conn.OpenChannel(newChannel.ChannelType(), nil) 103 }(newChannel) 104 105 defer channel.Close() 106 } 107 conn.Close() 108 }() 109 110 return l.Addr().String() 111 } 112 113 func TestNew_Invalid(t *testing.T) { 114 address := newMockLineServer(t) 115 parts := strings.Split(address, ":") 116 117 r := &terraform.InstanceState{ 118 Ephemeral: terraform.EphemeralState{ 119 ConnInfo: map[string]string{ 120 "type": "ssh", 121 "user": "user", 122 "password": "i-am-invalid", 123 "host": parts[0], 124 "port": parts[1], 125 "timeout": "30s", 126 }, 127 }, 128 } 129 130 c, err := New(r) 131 if err != nil { 132 t.Fatalf("error creating communicator: %s", err) 133 } 134 135 err = c.Connect(nil) 136 if err == nil { 137 t.Fatal("should have had an error connecting") 138 } 139 } 140 141 func TestStart(t *testing.T) { 142 address := newMockLineServer(t) 143 parts := strings.Split(address, ":") 144 145 r := &terraform.InstanceState{ 146 Ephemeral: terraform.EphemeralState{ 147 ConnInfo: map[string]string{ 148 "type": "ssh", 149 "user": "user", 150 "password": "pass", 151 "host": parts[0], 152 "port": parts[1], 153 "timeout": "30s", 154 }, 155 }, 156 } 157 158 c, err := New(r) 159 if err != nil { 160 t.Fatalf("error creating communicator: %s", err) 161 } 162 163 var cmd remote.Cmd 164 stdout := new(bytes.Buffer) 165 cmd.Command = "echo foo" 166 cmd.Stdout = stdout 167 168 err = c.Start(&cmd) 169 if err != nil { 170 t.Fatalf("error executing remote command: %s", err) 171 } 172 } 173 174 func TestAccUploadFile(t *testing.T) { 175 // use the local ssh server and scp binary to check uploads 176 if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" { 177 t.Log("Skipping Upload Acceptance without SSH_UPLOAD_TEST set") 178 t.Skip() 179 } 180 181 r := &terraform.InstanceState{ 182 Ephemeral: terraform.EphemeralState{ 183 ConnInfo: map[string]string{ 184 "type": "ssh", 185 "user": os.Getenv("USER"), 186 "host": "127.0.0.1", 187 "port": "22", 188 "timeout": "30s", 189 }, 190 }, 191 } 192 193 c, err := New(r) 194 if err != nil { 195 t.Fatalf("error creating communicator: %s", err) 196 } 197 198 tmpDir, err := ioutil.TempDir("", "communicator") 199 if err != nil { 200 t.Fatal(err) 201 } 202 defer os.RemoveAll(tmpDir) 203 204 content := []byte("this is the file content") 205 source := bytes.NewReader(content) 206 tmpFile := filepath.Join(tmpDir, "tempFile.out") 207 err = c.Upload(tmpFile, source) 208 if err != nil { 209 t.Fatalf("error uploading file: %s", err) 210 } 211 212 data, err := ioutil.ReadFile(tmpFile) 213 if err != nil { 214 t.Fatal(err) 215 } 216 217 if !bytes.Equal(data, content) { 218 t.Fatalf("bad: %s", data) 219 } 220 } 221 222 func TestAccHugeUploadFile(t *testing.T) { 223 // use the local ssh server and scp binary to check uploads 224 if ok := os.Getenv("SSH_UPLOAD_TEST"); ok == "" { 225 t.Log("Skipping Upload Acceptance without SSH_UPLOAD_TEST set") 226 t.Skip() 227 } 228 229 r := &terraform.InstanceState{ 230 Ephemeral: terraform.EphemeralState{ 231 ConnInfo: map[string]string{ 232 "type": "ssh", 233 "user": os.Getenv("USER"), 234 "host": "127.0.0.1", 235 "port": "22", 236 "timeout": "30s", 237 }, 238 }, 239 } 240 241 c, err := New(r) 242 if err != nil { 243 t.Fatalf("error creating communicator: %s", err) 244 } 245 246 // copy 4GB of data, random to prevent compression. 247 size := int64(1 << 32) 248 source := io.LimitReader(rand.New(rand.NewSource(0)), size) 249 250 dest, err := ioutil.TempFile("", "communicator") 251 if err != nil { 252 t.Fatal(err) 253 } 254 destName := dest.Name() 255 dest.Close() 256 defer os.Remove(destName) 257 258 t.Log("Uploading to", destName) 259 260 // bypass the Upload method so we can directly supply the file size 261 // preventing the extra copy of the huge file. 262 targetDir := filepath.Dir(destName) 263 targetFile := filepath.Base(destName) 264 265 scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error { 266 return scpUploadFile(targetFile, source, w, stdoutR, size) 267 } 268 269 err = c.scpSession("scp -vt "+targetDir, scpFunc) 270 if err != nil { 271 t.Fatal(err) 272 } 273 274 // check the final file size 275 fs, err := os.Stat(destName) 276 if err != nil { 277 t.Fatal(err) 278 } 279 280 if fs.Size() != size { 281 t.Fatalf("expected file size of %d, got %d", size, fs.Size()) 282 } 283 } 284 285 func TestScriptPath(t *testing.T) { 286 cases := []struct { 287 Input string 288 Pattern string 289 }{ 290 { 291 "/tmp/script.sh", 292 `^/tmp/script\.sh$`, 293 }, 294 { 295 "/tmp/script_%RAND%.sh", 296 `^/tmp/script_(\d+)\.sh$`, 297 }, 298 } 299 300 for _, tc := range cases { 301 r := &terraform.InstanceState{ 302 Ephemeral: terraform.EphemeralState{ 303 ConnInfo: map[string]string{ 304 "type": "ssh", 305 "script_path": tc.Input, 306 }, 307 }, 308 } 309 comm, err := New(r) 310 if err != nil { 311 t.Fatalf("err: %s", err) 312 } 313 output := comm.ScriptPath() 314 315 match, err := regexp.Match(tc.Pattern, []byte(output)) 316 if err != nil { 317 t.Fatalf("bad: %s\n\nerr: %s", tc.Input, err) 318 } 319 if !match { 320 t.Fatalf("bad: %s\n\n%s", tc.Input, output) 321 } 322 } 323 } 324 325 func TestScriptPath_randSeed(t *testing.T) { 326 // Pre GH-4186 fix, this value was the deterministic start the pseudorandom 327 // chain of unseeded math/rand values for Int31(). 328 staticSeedPath := "/tmp/terraform_1298498081.sh" 329 c, err := New(&terraform.InstanceState{}) 330 if err != nil { 331 t.Fatalf("err: %s", err) 332 } 333 path := c.ScriptPath() 334 if path == staticSeedPath { 335 t.Fatalf("rand not seeded! got: %s", path) 336 } 337 } 338 339 const testClientPrivateKey = `-----BEGIN RSA PRIVATE KEY----- 340 MIIEpQIBAAKCAQEAxOgNXOJ/jrRDxBZTSk2X9otNy9zcpUmJr5ifDi5sy7j2ZiQS 341 beBt1Wf+tLNWis8Cyq06ttEvjjRuM75yucyD6GrqDTXVCSm4PeOIQeDhPhw26wYZ 342 O0h/mFgrAiCwaEl8AFXVBInOhVn/0nqgUpkwckh76bjTsNeifkiugK3cfJOuBdrU 343 ZGbgugJjmYo4Cmv7ECo1gCFT5N+BAjOji3z3N5ClBH5HaWC77jH7kTH0k5cZ+ZRQ 344 tG9EqLyvWlnzTAR/Yly0JInkOa16Ms1Au5sIJwEoJfHKsNVK06IlLob53nblwYW0 345 H5gv1Kb/rS+nUkpPtA5YFShB7iZnPLPPv6qXSwIDAQABAoIBAC0UY1rMkB9/rbQK 346 2G6+bPgI1HrDydAdkeQdsOxyPH43jlG8GGwHYZ3l/S4pkLqewijcmACay6Rm5IP8 347 Kg/XfquLLqJvnKJIZuHkYaGTdn3dv8T21Hf6FRwvs0j9auW1TSpWfDpZwmpNPIBX 348 irTeVXUUmynbIrvt4km/IhRbuYrbbb964CLYD1DCl3XssXxoRNvPpc5EtOuyDorA 349 5g1hvZR1FqbOAmOuNQMYJociMuWB8mCaHb+o1Sg4A65OLXxoKs0cuwInJ/n/R4Z3 350 +GrV+x5ypBMxXgjjQtKMLEOujkvxs1cp34hkbhKMHHXxbMu5jl74YtGGsLLk90rq 351 ieZGIgECgYEA49OM9mMCrDoFUTZdJaSARA/MOXkdQgrqVTv9kUHee7oeMZZ6lS0i 352 bPU7g+Bq+UAN0qcw9x992eAElKjBA71Q5UbZYWh29BDMZd8bRJmwz4P6aSMoYLWI 353 Sr31caJU9LdmPFatarNeehjSJtlTuoZD9+NElnnUwNaTeOOo5UdhTQsCgYEA3UGm 354 QWoDUttFwK9oL2KL8M54Bx6EzNhnyk03WrqBbR7PJcPKnsF0R/0soQ+y0FW0r8RJ 355 TqG6ze5fUJII72B4GlMTQdP+BIvaKQttwWQTNIjbbv4NksF445gdVOO1xi9SvQ7k 356 uvMVxOb+1jL3HAFa3furWu2tJRDs6dhuaILLxsECgYEAhnhlKUBDYZhVbxvhWsh/ 357 lKymY/3ikQqUSX7BKa1xPiIalDY3YDllql4MpMgfG8L85asdMZ96ztB0o7H/Ss/B 358 IbLxt5bLLz+DBVXsaE82lyVU9h10RbCgI01/w3SHJHHjfBXFAcehKfvgfmGkE+IP 359 2A5ie1aphrCgFqh5FetNuQUCgYEAibL42I804FUtFR1VduAa/dRRqQSaW6528dWa 360 lLGsKRBalUNEEAeP6dmr89UEUVp1qEo94V0QGGe5FDi+rNPaC3AWdQqNdaDgNlkx 361 hoFU3oYqIuqj4ejc5rBd2N4a2+vJz3W8bokozDGC+iYf2mMRfUPKwj1XW9Er0OFs 362 3UhBsEECgYEAto/iJB7ZlCM7EyV9JW0tsEt83rbKMQ/Ex0ShbBIejej0Xx7bwx60 363 tVgay+bzJnNkXu6J4XVI98A/WsdI2kW4hL0STYdHV5HVA1l87V4ZbvTF2Bx8a8RJ 364 OF3UjpMTWKqOprw9nAu5VuwNRVzORF8ER8rgGeaR2/gsSvIYFy9VXq8= 365 -----END RSA PRIVATE KEY-----` 366 367 var testClientPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDE6A1c4n+OtEPEFlNKTZf2i03L3NylSYmvmJ8OLmzLuPZmJBJt4G3VZ/60s1aKzwLKrTq20S+ONG4zvnK5zIPoauoNNdUJKbg944hB4OE+HDbrBhk7SH+YWCsCILBoSXwAVdUEic6FWf/SeqBSmTBySHvpuNOw16J+SK6Ardx8k64F2tRkZuC6AmOZijgKa/sQKjWAIVPk34ECM6OLfPc3kKUEfkdpYLvuMfuRMfSTlxn5lFC0b0SovK9aWfNMBH9iXLQkieQ5rXoyzUC7mwgnASgl8cqw1UrToiUuhvneduXBhbQfmC/Upv+tL6dSSk+0DlgVKEHuJmc8s8+/qpdL` 368 369 func acceptUserPass(goodUser, goodPass string) func(ssh.ConnMetadata, []byte) (*ssh.Permissions, error) { 370 return func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { 371 if c.User() == goodUser && string(pass) == goodPass { 372 return nil, nil 373 } 374 return nil, fmt.Errorf("password rejected for %q", c.User()) 375 } 376 } 377 378 func acceptPublicKey(keystr string) func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) { 379 goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr)) 380 if err != nil { 381 panic(fmt.Errorf("error parsing key: %s", err)) 382 } 383 return func(_ ssh.ConnMetadata, inkey ssh.PublicKey) (*ssh.Permissions, error) { 384 if bytes.Compare(inkey.Marshal(), goodkey.Marshal()) == 0 { 385 return nil, nil 386 } 387 388 return nil, fmt.Errorf("public key rejected") 389 } 390 }