e2e/revoke_test.go

32a5f76e5b270097b63a5bba9a43557cf50ad63d
gitbay/e2e/revoke_test.go history · blame · raw

155 lines · 4337 bytes

  1package e2e
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"io"
  7	"os"
  8	"os/exec"
  9	"path/filepath"
 10	"strconv"
 11	"strings"
 12	"testing"
 13	"time"
 14)
 15
 16// pkt frames one pkt-line.
 17func pkt(s string) string { return fmt.Sprintf("%04x%s", len(s)+4, s) }
 18
 19// readPkt reads one pkt-line; a flush reads as "".
 20func readPkt(r *bufio.Reader) (string, error) {
 21	var n [4]byte
 22	if _, err := io.ReadFull(r, n[:]); err != nil {
 23		return "", err
 24	}
 25	size, err := strconv.ParseUint(string(n[:]), 16, 16)
 26	if err != nil {
 27		return "", err
 28	}
 29	if size == 0 {
 30		return "", nil
 31	}
 32	buf := make([]byte, size-4)
 33	_, err = io.ReadFull(r, buf)
 34	return string(buf), err
 35}
 36
 37// fingerprint is the SHA256 fingerprint of a public key file.
 38func fingerprint(t *testing.T, pubPath string) string {
 39	t.Helper()
 40	out, err := exec.Command("ssh-keygen", "-lf", pubPath).Output()
 41	if err != nil {
 42		t.Fatalf("ssh-keygen -lf: %v", err)
 43	}
 44	return strings.Fields(string(out))[1]
 45}
 46
 47// Removing a key cuts the connections it opened: every session
 48// multiplexed on a ControlMaster, and a push in flight, which moves no
 49// ref (#256).
 50func TestRemovedKeyCutsMultiplexedConnection(t *testing.T) {
 51	t.Parallel()
 52	inst := startInstance(t)
 53	aliceKey := setupPublicRepo(t, inst, "alice/app")
 54	spare := inst.newKey(t, "spare")
 55	pub, err := os.ReadFile(spare + ".pub")
 56	if err != nil {
 57		t.Fatal(err)
 58	}
 59	if _, errOut, code := inst.ssh(t, aliceKey, string(pub), "keys", "add"); code != 0 {
 60		t.Fatalf("keys add: %s", errOut)
 61	}
 62
 63	// The control socket sits under the system temp dir with a short
 64	// name: t.TempDir() or %C on macOS passes the 104-byte socket path
 65	// limit.
 66	cmDir, err := os.MkdirTemp("", "cm")
 67	if err != nil {
 68		t.Fatal(err)
 69	}
 70	t.Cleanup(func() { os.RemoveAll(cmDir) })
 71	muxArgs := []string{
 72		"-p", fmt.Sprint(inst.port),
 73		"-i", aliceKey,
 74		"-o", "IdentitiesOnly=yes",
 75		"-o", "StrictHostKeyChecking=no",
 76		"-o", "UserKnownHostsFile=" + filepath.Join(inst.sshDir, "known_hosts"),
 77		"-o", "BatchMode=yes",
 78		"-o", "ControlMaster=auto",
 79		"-o", "ControlPath=" + filepath.Join(cmDir, "s"),
 80		"-o", "ControlPersist=60",
 81	}
 82	mux := func(args ...string) *exec.Cmd {
 83		return exec.Command("ssh", append(append([]string{}, muxArgs...), args...)...)
 84	}
 85	t.Cleanup(func() { mux("-O", "exit", "git@127.0.0.1").Run() })
 86
 87	if out, err := mux("git@127.0.0.1", "whoami").Output(); err != nil || strings.TrimSpace(string(out)) != "alice" {
 88		var stderr []byte
 89		if ee, ok := err.(*exec.ExitError); ok {
 90			stderr = ee.Stderr
 91		}
 92		t.Fatalf("whoami over the master: %v %q %s", err, out, stderr)
 93	}
 94
 95	// A push held open mid-pack: the ref update is sent, the pack is not.
 96	push := mux("git@127.0.0.1", "git-receive-pack", "alice/app")
 97	stdin, err := push.StdinPipe()
 98	if err != nil {
 99		t.Fatal(err)
100	}
101	stdout, err := push.StdoutPipe()
102	if err != nil {
103		t.Fatal(err)
104	}
105	if err := push.Start(); err != nil {
106		t.Fatal(err)
107	}
108	t.Cleanup(func() { push.Process.Kill() })
109	adv := bufio.NewReader(stdout)
110	first, err := readPkt(adv)
111	if err != nil || len(first) < 40 {
112		t.Fatalf("advertisement: %q %v", first, err)
113	}
114	oldSHA := first[:40]
115	for {
116		line, err := readPkt(adv)
117		if err != nil {
118			t.Fatalf("advertisement: %v", err)
119		}
120		if line == "" {
121			break
122		}
123	}
124	newSHA := strings.Repeat("1", 40)
125	io.WriteString(stdin, pkt(oldSHA+" "+newSHA+" refs/heads/main\x00report-status\n")+"0000")
126	// A pack header announcing one object, and no object.
127	stdin.Write([]byte("PACK\x00\x00\x00\x02\x00\x00\x00\x01"))
128	exited := make(chan error, 1)
129	go func() {
130		io.Copy(io.Discard, adv)
131		exited <- push.Wait()
132	}()
133
134	if _, errOut, code := inst.ssh(t, spare, "", "keys", "remove", fingerprint(t, aliceKey+".pub")); code != 0 {
135		t.Fatalf("keys remove: %s", errOut)
136	}
137	select {
138	case err := <-exited:
139		if err == nil {
140			t.Fatal("the push exited cleanly after its key was removed")
141		}
142	case <-time.After(10 * time.Second):
143		t.Fatal("the push outlived its key")
144	}
145
146	// The master went with the connection; a new one authenticates
147	// again, and the key is unknown.
148	if out, err := mux("git@127.0.0.1", "whoami").CombinedOutput(); err == nil {
149		t.Fatalf("whoami after removal succeeded: %s", out)
150	}
151	refs := mustGit(t, t.TempDir(), inst.gitEnv(spare), "ls-remote", inst.sshURL("alice/app"), "refs/heads/main")
152	if !strings.HasPrefix(refs, oldSHA) {
153		t.Fatalf("main moved: %s, want %s", refs, oldSHA)
154	}
155}