diff options
author | Nick Thomas <nick@gitlab.com> | 2021-03-13 01:19:20 +0000 |
---|---|---|
committer | Nick Thomas <nick@gitlab.com> | 2021-03-16 22:27:31 +0000 |
commit | 05635a49530760e79b4a5aca2a8b96aca8c11440 (patch) | |
tree | f00ed19dc9b0ca75bf83fd6d30aa8cc32e33299d | |
parent | dff2209698bea83dc4ef0b31c3887be7efd34166 (diff) | |
download | gitlab-shell-500-gitlab-sshd-acceptance-tests.tar.gz |
gitlab-sshd: Acceptance test for the discover command500-gitlab-sshd-acceptance-tests
With this, we can start to build confidence in making changes to
gitlab-sshd.
-rw-r--r-- | cmd/gitlab-sshd/acceptance_test.go | 190 | ||||
-rw-r--r-- | go.mod | 1 | ||||
-rw-r--r-- | go.sum | 2 | ||||
-rw-r--r-- | internal/sshd/sshd.go | 2 |
4 files changed, 195 insertions, 0 deletions
diff --git a/cmd/gitlab-sshd/acceptance_test.go b/cmd/gitlab-sshd/acceptance_test.go new file mode 100644 index 0000000..aaba427 --- /dev/null +++ b/cmd/gitlab-sshd/acceptance_test.go @@ -0,0 +1,190 @@ +package main_test + +import ( + "bufio" + "context" + "crypto/ed25519" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "testing" + + "github.com/mikesmitty/edkey" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +var ( + sshdPath = "" +) + +func init() { + rootDir := rootDir() + sshdPath = filepath.Join(rootDir, "bin", "gitlab-sshd") + + if _, err := os.Stat(sshdPath); os.IsNotExist(err) { + panic(fmt.Errorf("cannot find executable %s. Please run 'make compile'", sshdPath)) + } +} + +func rootDir() string { + _, currentFile, _, ok := runtime.Caller(0) + if !ok { + panic(fmt.Errorf("rootDir: calling runtime.Caller failed")) + } + + return filepath.Join(filepath.Dir(currentFile), "..", "..") +} + +func successAPI(t *testing.T) http.Handler { + t.Helper() + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("gitlab-api-mock: received request: %s %s", r.Method, r.RequestURI) + w.Header().Set("Content-Type", "application/json") + + switch r.URL.EscapedPath() { + case "/api/v4/internal/authorized_keys": + fmt.Fprintf(w, `{"id":1, "key":"%s"}`, r.FormValue("key")) + case "/api/v4/internal/discover": + fmt.Fprint(w, `{"id": 1000, "name": "Test User", "username": "test-user"}`) + default: + w.WriteHeader(404) + fmt.Fprint(w, `{}`) + } + }) +} + +func genServerConfig(gitlabUrl, hostKeyPath string) []byte { + return []byte(`--- +user: "git" +log_file: "" +log_format: json +secret: "0123456789abcdef" +gitlab_url: "` + gitlabUrl + `" +sshd: + listen: "127.0.0.1:0" + web_listen: "" + host_key_files: + - "` + hostKeyPath + `"`) +} + +func buildClient(t *testing.T, addr string, hostKey ed25519.PublicKey) *ssh.Client { + t.Helper() + + pubKey, err := ssh.NewPublicKey(hostKey) + require.NoError(t, err) + + _, clientPrivKey, err := ed25519.GenerateKey(nil) + clientSigner, err := ssh.NewSignerFromKey(clientPrivKey) + require.NoError(t, err) + + client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{ + User: "git", + Auth: []ssh.AuthMethod{ssh.PublicKeys(clientSigner)}, + HostKeyCallback: ssh.FixedHostKey(pubKey), + }) + require.NoError(t, err) + + t.Cleanup(func() { client.Close() }) + + return client +} + +func configureSSHD(t *testing.T, apiServer string) (string, ed25519.PublicKey) { + t.Helper() + + dir, err := ioutil.TempDir("", "gitlab-sshd-acceptance-test-") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(dir) }) + + configFile := filepath.Join(dir, "config.yml") + hostKeyFile := filepath.Join(dir, "hostkey") + + pub, priv, err := ed25519.GenerateKey(nil) + require.NoError(t, err) + + configFileData := genServerConfig(apiServer, hostKeyFile) + require.NoError(t, ioutil.WriteFile(configFile, configFileData, 0644)) + + block := &pem.Block{Type: "OPENSSH PRIVATE KEY", Bytes: edkey.MarshalED25519PrivateKey(priv)} + hostKeyData := pem.EncodeToMemory(block) + require.NoError(t, ioutil.WriteFile(hostKeyFile, hostKeyData, 0400)) + + return dir, pub +} + +func startSSHD(t *testing.T, dir string) string { + t.Helper() + + // We need to scan the first few lines of stderr to get the listen address. + // Once we've learned it, we'll start a goroutine to copy everything to + // the real stderr + pr, pw := io.Pipe() + t.Cleanup(func() { pr.Close() }) + t.Cleanup(func() { pw.Close() }) + + scanner := bufio.NewScanner(pr) + extractor := regexp.MustCompile(`msg="Listening on ([0-9a-f\[\]\.:]+)"`) + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, sshdPath, "-config-dir", dir) + cmd.Stdout = os.Stdout + cmd.Stderr = pw + require.NoError(t, cmd.Start()) + t.Logf("gitlab-sshd: Start(): success") + t.Cleanup(func() { t.Logf("gitlab-sshd: Wait(): %v", cmd.Wait()) }) + t.Cleanup(cancel) + + var listenAddr string + for scanner.Scan() { + if matches := extractor.FindSubmatch(scanner.Bytes()); len(matches) == 2 { + listenAddr = string(matches[1]) + break + } + } + require.NotEmpty(t, listenAddr, "Couldn't extract listen address from gitlab-sshd") + + go func() { io.Copy(os.Stderr, pr) }() + + return listenAddr +} + +// Starts an instance of gitlab-sshd with the given arguments, returning an SSH +// client already connected to it +func runSSHD(t *testing.T, apiHandler http.Handler) *ssh.Client { + t.Helper() + + // Set up a stub gitlab server + apiServer := httptest.NewServer(apiHandler) + t.Logf("gitlab-api-mock: started: url=%q", apiServer.URL) + t.Cleanup(func() { + apiServer.Close() + t.Logf("gitlab-api-mock: closed") + }) + + dir, hostKey := configureSSHD(t, apiServer.URL) + listenAddr := startSSHD(t, dir) + + return buildClient(t, listenAddr, hostKey) +} + +func TestDiscoverSuccess(t *testing.T) { + client := runSSHD(t, successAPI(t)) + + session, err := client.NewSession() + require.NoError(t, err) + defer session.Close() + + output, err := session.Output("discover") + require.NoError(t, err) + require.Equal(t, "Welcome to GitLab, @test-user!\n", string(output)) +} @@ -4,6 +4,7 @@ go 1.13 require ( github.com/mattn/go-shellwords v1.0.11 + github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/otiai10/copy v1.4.2 github.com/prometheus/client_golang v1.9.0 github.com/sirupsen/logrus v1.7.0 @@ -319,6 +319,8 @@ github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go. github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= diff --git a/internal/sshd/sshd.go b/internal/sshd/sshd.go index 74b4bf4..8775e35 100644 --- a/internal/sshd/sshd.go +++ b/internal/sshd/sshd.go @@ -73,6 +73,8 @@ func Run(cfg *config.Config) error { return fmt.Errorf("failed to listen for connection: %w", err) } + log.Infof("Listening on %v", sshListener.Addr().String()) + config := &ssh.ServerConfig{ PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { if conn.User() != cfg.User { |