<commit_before>// +build !race

package ssh

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/hashicorp/terraform/communicator/remote"
	"github.com/hashicorp/terraform/terraform"
	"golang.org/x/crypto/ssh"
)

// private key for mock server
const testServerPrivateKey = `-----<KEY>`

var serverConfig = &ssh.ServerConfig{
	PasswordCallback:  acceptUserPass("user", "<PASSWORD>"),
	PublicKeyCallback: acceptPublicKey(testClientPublicKey),
}

func init() {
	// Parse and set the private key of the server, required to accept connections
	signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
	if err != nil {
		panic("unable to parse private key: " + err.Error())
	}
	serverConfig.AddHostKey(signer)
}

func newMockLineServer(t *testing.T) string {
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("Unable to listen for connection: %s", err)
	}

	go func() {
		defer l.Close()
		c, err := l.Accept()
		if err != nil {
			t.Errorf("Unable to accept incoming connection: %s", err)
		}
		defer c.Close()
		conn, chans, _, err := ssh.NewServerConn(c, serverConfig)
		if err != nil {
			t.Logf("Handshaking error: %v", err)
		}
		t.Log("Accepted SSH connection")

		for newChannel := range chans {
			channel, requests, err := newChannel.Accept()
			if err != nil {
				t.Errorf("Unable to accept channel.")
			}
			t.Log("Accepted channel")

			go func(in <-chan *ssh.Request) {
				for req := range in {
					if req.WantReply {
						req.Reply(true, nil)
					}
				}
			}(requests)

			go func(newChannel ssh.NewChannel) {
				conn.OpenChannel(newChannel.ChannelType(), nil)
			}(newChannel)

			defer channel.Close()
		}
		conn.Close()
	}()

	return l.Addr().String()
}

func TestNew_Invalid(t *testing.T) {
	address := newMockLineServer(t)
	parts := strings.Split(address, ":")

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":     "ssh",
				"user":     "user",
				"password": "<PASSWORD>",
				"host":     parts[0],
				"port":     parts[1],
				"timeout":  "<PASSWORD>",
			},
		},
	}

	c, err := New(r)
	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 TestStart(t *testing.T) {
	address := newMockLineServer(t)
	parts := strings.Split(address, ":")

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":     "ssh",
				"user":     "user",
				"password": "<PASSWORD>",
				"host":     parts[0],
				"port":     parts[1],
				"timeout":  "30s",
			},
		},
	}

	c, err := New(r)
	if err != nil {
		t.Fatalf("error creating communicator: %s", 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)
	}
}

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

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":    "ssh",
				"user":    os.Getenv("USER"),
				"host":    "127.0.0.1",
				"port":    "22",
				"timeout": "30s",
			},
		},
	}

	c, err := New(r)
	if err != nil {
		t.Fatalf("error creating communicator: %s", err)
	}

	tmpDir, err := ioutil.TempDir("", "communicator")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	content := []byte("this is the file content")
	source := bytes.NewReader(content)
	tmpFile := filepath.Join(tmpDir, "tempFile.out")
	err = c.Upload(tmpFile, source)
	if err != nil {
		t.Fatalf("error uploading file: %s", err)
	}

	data, err := ioutil.ReadFile(tmpFile)
	if err != nil {
		t.Fatal(err)
	}

	if !bytes.Equal(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()
	}

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":    "ssh",
				"user":    os.Getenv("USER"),
				"host":    "127.0.0.1",
				"port":    "22",
				"timeout": "30s",
			},
		},
	}

	c, err := New(r)
	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 := ioutil.TempFile("", "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)
	}

	err = c.scpSession("scp -vt "+targetDir, 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 {
		r := &terraform.InstanceState{
			Ephemeral: terraform.EphemeralState{
				ConnInfo: map[string]string{
					"type":        "ssh",
					"script_path": tc.Input,
				},
			},
		}
		comm, err := New(r)
		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(&terraform.InstanceState{})
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	path := c.ScriptPath()
	if path == staticSeedPath {
		t.Fatalf("rand not seeded! got: %s", path)
	}
}

const testClientPrivateKey = `-----<KEY>`

var testClientPublicKey = `ssh-rsa <KEY>`

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) {
	goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr))
	if err != nil {
		panic(fmt.Errorf("error parsing key: %s", err))
	}
	return func(_ ssh.ConnMetadata, inkey ssh.PublicKey) (*ssh.Permissions, error) {
		if bytes.Equal(inkey.Marshal(), goodkey.Marshal()) {
			return nil, nil
		}

		return nil, fmt.Errorf("public key rejected")
	}
}
<commit_msg>add test for host key validation<commit_after>// +build !race

package ssh

import (
	"bufio"
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"testing"

	"github.com/hashicorp/terraform/communicator/remote"
	"github.com/hashicorp/terraform/terraform"
	"golang.org/x/crypto/ssh"
)

// private key for mock server
const testServerPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
<KEY>
-----END RSA PRIVATE KEY-----`

var serverConfig = &ssh.ServerConfig{
	PasswordCallback:  acceptUserPass("user", "pass"),
	PublicKeyCallback: acceptPublicKey(testClientPublicKey),
}

func init() {
	// Parse and set the private key of the server, required to accept connections
	signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
	if err != nil {
		panic("unable to parse private key: " + err.Error())
	}
	serverConfig.AddHostKey(signer)
}

func newMockLineServer(t *testing.T) string {
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("Unable to listen for connection: %s", err)
	}

	go func() {
		defer l.Close()
		c, err := l.Accept()
		if err != nil {
			t.Errorf("Unable to accept incoming connection: %s", err)
		}
		defer c.Close()
		conn, chans, _, err := ssh.NewServerConn(c, serverConfig)
		if err != nil {
			t.Logf("Handshaking error: %v", err)
		}
		t.Log("Accepted SSH connection")

		for newChannel := range chans {
			channel, requests, err := newChannel.Accept()
			if err != nil {
				t.Errorf("Unable to accept channel.")
			}
			t.Log("Accepted channel")

			go func(in <-chan *ssh.Request) {
				for req := range in {
					if req.WantReply {
						req.Reply(true, nil)
					}
				}
			}(requests)

			go func(newChannel ssh.NewChannel) {
				conn.OpenChannel(newChannel.ChannelType(), nil)
			}(newChannel)

			defer channel.Close()
		}
		conn.Close()
	}()

	return l.Addr().String()
}

func TestNew_Invalid(t *testing.T) {
	address := newMockLineServer(t)
	parts := strings.Split(address, ":")

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":     "ssh",
				"user":     "user",
				"password": "i-am-invalid",
				"host":     parts[0],
				"port":     parts[1],
				"timeout":  "30s",
			},
		},
	}

	c, err := New(r)
	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 TestStart(t *testing.T) {
	address := newMockLineServer(t)
	parts := strings.Split(address, ":")

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":     "ssh",
				"user":     "user",
				"password": "<PASSWORD>",
				"host":     parts[0],
				"port":     parts[1],
				"timeout":  "30s",
			},
		},
	}

	c, err := New(r)
	if err != nil {
		t.Fatalf("error creating communicator: %s", 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)
	}
}

func TestHostKey(t *testing.T) {
	// get the server's public key
	signer, err := ssh.ParsePrivateKey([]byte(testServerPrivateKey))
	if err != nil {
		panic("unable to parse private key: " + err.Error())
	}
	pubKey := fmt.Sprintf("ssh-rsa %s", base64.StdEncoding.EncodeToString(signer.PublicKey().Marshal()))

	address := newMockLineServer(t)
	host, p, _ := net.SplitHostPort(address)
	port, _ := strconv.Atoi(p)

	connInfo := &connectionInfo{
		User:     "user",
		Password: "<PASSWORD>",
		Host:     host,
		HostKey:  pubKey,
		Port:     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)
	}

	// now check with the wrong HostKey
	address = newMockLineServer(t)
	_, p, _ = net.SplitHostPort(address)
	port, _ = strconv.Atoi(p)

	connInfo.HostKey = testClientPublicKey
	connInfo.Port = 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 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()
	}

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":    "ssh",
				"user":    os.Getenv("USER"),
				"host":    "127.0.0.1",
				"port":    "22",
				"timeout": "30s",
			},
		},
	}

	c, err := New(r)
	if err != nil {
		t.Fatalf("error creating communicator: %s", err)
	}

	tmpDir, err := ioutil.TempDir("", "communicator")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	content := []byte("this is the file content")
	source := bytes.NewReader(content)
	tmpFile := filepath.Join(tmpDir, "tempFile.out")
	err = c.Upload(tmpFile, source)
	if err != nil {
		t.Fatalf("error uploading file: %s", err)
	}

	data, err := ioutil.ReadFile(tmpFile)
	if err != nil {
		t.Fatal(err)
	}

	if !bytes.Equal(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()
	}

	r := &terraform.InstanceState{
		Ephemeral: terraform.EphemeralState{
			ConnInfo: map[string]string{
				"type":    "ssh",
				"user":    os.Getenv("USER"),
				"host":    "127.0.0.1",
				"port":    "22",
				"timeout": "30s",
			},
		},
	}

	c, err := New(r)
	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 := ioutil.TempFile("", "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)
	}

	err = c.scpSession("scp -vt "+targetDir, 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 {
		r := &terraform.InstanceState{
			Ephemeral: terraform.EphemeralState{
				ConnInfo: map[string]string{
					"type":        "ssh",
					"script_path": tc.Input,
				},
			},
		}
		comm, err := New(r)
		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(&terraform.InstanceState{})
	if err != nil {
		t.Fatalf("err: %s", err)
	}
	path := c.ScriptPath()
	if path == staticSeedPath {
		t.Fatalf("rand not seeded! got: %s", path)
	}
}

const testClientPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
<KEY>
-----END RSA PRIVATE KEY-----`

var testClientPublicKey = `ssh-rsa <KEY>`

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) {
	goodkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keystr))
	if err != nil {
		panic(fmt.Errorf("error parsing key: %s", err))
	}
	return func(_ ssh.ConnMetadata, inkey ssh.PublicKey) (*ssh.Permissions, error) {
		if bytes.Equal(inkey.Marshal(), goodkey.Marshal()) {
			return nil, nil
		}

		return nil, fmt.Errorf("public key rejected")
	}
}

{'fraction_non_alphanumeric': 0.1698901623686724, 'fraction_numerical': 0.0071633237822349575, 'mean_word_length': 3.3684485006518905, 'pattern_counts': {'":': 52, '<': 21, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 6, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3613612', 'n_tokens_mistral': 7019, 'n_tokens_neox': 6335, 'n_words': 1510}