e2e/http_test.go
165 lines · 5828 bytes
6 symbols in this file
1package e2e
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "testing"
12)
13
14// gitBinaries returns every distinct git on this machine, so transport
15// behavior is verified against more than one client version.
16func gitBinaries() []string {
17 bins := []string{"git"}
18 if _, err := os.Stat("/usr/bin/git"); err == nil {
19 bins = append(bins, "/usr/bin/git")
20 }
21 return bins
22}
23
24// setupPublicRepo creates alice with a public repo containing one commit and
25// returns her key path.
26func setupPublicRepo(t *testing.T, inst *instance, repo string) string {
27 t.Helper()
28 aliceKey := inst.newKey(t, "alice")
29 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
30 _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", repo)
31 if code != 0 {
32 t.Fatalf("repo create: %s", errOut)
33 }
34 work := t.TempDir()
35 env := inst.gitEnv(aliceKey)
36 mustGit(t, work, env, "clone", inst.sshURL(repo), "w")
37 dir := filepath.Join(work, "w")
38 if err := os.WriteFile(filepath.Join(dir, "README"), []byte("public\n"), 0o644); err != nil {
39 t.Fatal(err)
40 }
41 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
42 mustGit(t, dir, env, "add", "README")
43 mustGit(t, dir, env, "commit", "-q", "-m", "init")
44 mustGit(t, dir, env, "push", "-q", "origin", "main")
45 return aliceKey
46}
47
48// anonEnv is a git environment with no credentials and prompting hard-failed:
49// if git ever tries to ask for a username or password, the command errors
50// with a distinctive message instead of hanging.
51func anonEnv() []string {
52 return append(os.Environ(),
53 "GIT_TERMINAL_PROMPT=0",
54 "GIT_ASKPASS=false",
55 "GIT_CONFIG_NOSYSTEM=1",
56 "GIT_CONFIG_GLOBAL=/dev/null", // no ~/.gitconfig credential helpers or signing
57 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
58 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test",
59 )
60}
61
62func (i *instance) httpURL(repo string) string {
63 return fmt.Sprintf("http://127.0.0.1:%d/%s.git", i.httpPort, repo)
64}
65
66func TestHTTPTransport(t *testing.T) {
67 t.Parallel()
68 inst := startInstance(t)
69 aliceKey := setupPublicRepo(t, inst, "alice/pub")
70
71 // Anonymous clone of a public repo over HTTP.
72 work := t.TempDir()
73 mustGit(t, work, anonEnv(), "clone", inst.httpURL("alice/pub"), "c")
74 dir := filepath.Join(work, "c")
75 if data, err := os.ReadFile(filepath.Join(dir, "README")); err != nil || string(data) != "public\n" {
76 t.Fatalf("cloned content wrong: %q, %v", data, err)
77 }
78
79 // Push over HTTP: fatal remote error with the SSH URL, no credential
80 // prompting of any kind — checked against every git version on this
81 // machine (the pkt-line ERR mechanism must be version-independent).
82 mustGit(t, dir, anonEnv(), "commit", "-q", "--allow-empty", "-m", "x")
83 for _, gitBin := range gitBinaries() {
84 cmd := exec.Command(gitBin, "push", "origin", "main")
85 cmd.Dir = dir
86 cmd.Env = anonEnv()
87 rawOut, err := cmd.CombinedOutput()
88 out := string(rawOut)
89 if err == nil {
90 t.Fatalf("[%s] push over http succeeded", gitBin)
91 }
92 if !strings.Contains(out, "remote error:") ||
93 !strings.Contains(out, "pushes to this forge go over SSH") ||
94 !strings.Contains(out, "git@gitbay.test:alice/pub.git") {
95 t.Fatalf("[%s] push refusal output:\n%s", gitBin, out)
96 }
97 for _, banned := range []string{"Username", "Password", "Authentication failed", "terminal prompts disabled", "401", "403"} {
98 if strings.Contains(out, banned) {
99 t.Fatalf("[%s] push refusal fell into credential path (%q):\n%s", gitBin, banned, out)
100 }
101 }
102 }
103
104 // Private repo: 404 on the wire for anonymous HTTP, for both services
105 // and for a nonexistent repo — all indistinguishable.
106 _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private")
107 if code != 0 {
108 t.Fatalf("create private: %s", errOut)
109 }
110 for _, u := range []string{
111 inst.httpURL("alice/secret") + "/info/refs?service=git-upload-pack",
112 inst.httpURL("alice/secret") + "/info/refs?service=git-receive-pack",
113 inst.httpURL("alice/nonexistent") + "/info/refs?service=git-upload-pack",
114 } {
115 resp, err := http.Get(u)
116 if err != nil {
117 t.Fatal(err)
118 }
119 body, _ := io.ReadAll(resp.Body)
120 resp.Body.Close()
121 if resp.StatusCode != http.StatusNotFound {
122 t.Fatalf("GET %s = %d, want 404\n%s", u, resp.StatusCode, body)
123 }
124 }
125 if out, code := gitRun(t, t.TempDir(), anonEnv(), "clone", inst.httpURL("alice/secret")); code == 0 {
126 t.Fatalf("anonymous clone of private repo succeeded:\n%s", out)
127 }
128}
129
130func TestGitDaemon(t *testing.T) {
131 t.Parallel()
132 inst := startInstance(t)
133 aliceKey := setupPublicRepo(t, inst, "alice/pub")
134 gitURL := func(repo string) string {
135 return fmt.Sprintf("git://127.0.0.1:%d/%s.git", inst.gitPort, repo)
136 }
137
138 // Not opted in yet: refused even though public.
139 if out, code := gitRun(t, t.TempDir(), anonEnv(), "clone", gitURL("alice/pub")); code == 0 {
140 t.Fatalf("git:// clone before opt-in succeeded:\n%s", out)
141 } else if !strings.Contains(out, "repository not exported") {
142 t.Fatalf("opt-out message:\n%s", out)
143 }
144
145 // Opt in, clone works.
146 _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/pub", "on")
147 if code != 0 {
148 t.Fatalf("git-daemon on: %s", errOut)
149 }
150 work := t.TempDir()
151 mustGit(t, work, anonEnv(), "clone", gitURL("alice/pub"), "c")
152 if data, _ := os.ReadFile(filepath.Join(work, "c", "README")); string(data) != "public\n" {
153 t.Fatalf("git:// clone content wrong: %q", data)
154 }
155
156 // Private repos cannot be opted in.
157 _, _, code = inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private")
158 if code != 0 {
159 t.Fatal("create private failed")
160 }
161 _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/secret", "on")
162 if code != 2 || !strings.Contains(errOut, "only public repositories") {
163 t.Fatalf("private opt-in: exit %d, %s", code, errOut)
164 }
165}