krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: e2e/acme_test.go · raw
1package e2e
2
3import (
4 "crypto/tls"
5 "fmt"
6 "net"
7 "net/http"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "testing"
12 "time"
13)
14
15// TestACMEServe verifies the acme wiring offline: the HTTPS listener is up
16// with autocert answering handshakes, and the port-80-style helper listener
17// serves redirects. Actual issuance needs a reachable CA and a public DNS
18// name, which a test cannot have; what matters here is that the plumbing is
19// correct and failure to issue does not kill the daemon.
20func TestACMEServe(t *testing.T) {
21 inst := startInstanceWith(t, "") // helper for binary + keys; killed below
22 inst.proc.Process.Kill()
23 inst.proc.Wait()
24
25 httpsPort := freePort(t)
26 acmeHTTPPort := freePort(t)
27 cfg := fmt.Sprintf(`
28[server]
29root = %q
30site_url = "https://gitbay.example"
31[ssh]
32port = %d
33[http]
34addr = "127.0.0.1:%d"
35tls = "acme"
36acme_email = "noreply@gitbay.example"
37acme_http_addr = "127.0.0.1:%d"
38`, inst.root, inst.port, httpsPort, acmeHTTPPort)
39 if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
40 t.Fatal(err)
41 }
42 inst.proc = exec.Command(inst.gitbayd, "--config", inst.config, "serve")
43 inst.proc.Stderr = os.Stderr
44 if err := inst.proc.Start(); err != nil {
45 t.Fatal(err)
46 }
47 t.Cleanup(func() { inst.proc.Process.Kill(); inst.proc.Wait() })
48
49 wait := func(port int) {
50 t.Helper()
51 deadline := time.Now().Add(10 * time.Second)
52 for {
53 conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 200*time.Millisecond)
54 if err == nil {
55 conn.Close()
56 return
57 }
58 if time.Now().After(deadline) {
59 t.Fatalf("port %d never came up", port)
60 }
61 time.Sleep(50 * time.Millisecond)
62 }
63 }
64 wait(httpsPort)
65 wait(acmeHTTPPort)
66
67 // The helper listener redirects everything to the canonical HTTPS host.
68 client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
69 return http.ErrUseLastResponse
70 }}
71 resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/repo/log?x=1", acmeHTTPPort))
72 if err != nil {
73 t.Fatal(err)
74 }
75 resp.Body.Close()
76 if resp.StatusCode != http.StatusMovedPermanently ||
77 resp.Header.Get("Location") != "https://gitbay.example/alice/repo/log?x=1" {
78 t.Fatalf("redirect: %d %q", resp.StatusCode, resp.Header.Get("Location"))
79 }
80
81 // A TLS handshake reaches autocert, which tries (and fails) to issue —
82 // the handshake errors, the daemon survives, the listener stays up.
83 conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp",
84 fmt.Sprintf("127.0.0.1:%d", httpsPort),
85 &tls.Config{ServerName: "gitbay.example", InsecureSkipVerify: true})
86 if err == nil {
87 conn.Close()
88 t.Fatal("handshake unexpectedly succeeded with no CA reachable")
89 }
90 wait(httpsPort) // still listening after the failed handshake
91
92 // Certificates cache under the server root.
93 if _, err := os.Stat(filepath.Join(inst.root, "acme")); err != nil {
94 t.Fatalf("acme cache dir: %v", err)
95 }
96
97 // A host outside the whitelist is refused before any issuance attempt.
98 conn2, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp",
99 fmt.Sprintf("127.0.0.1:%d", httpsPort),
100 &tls.Config{ServerName: "evil.example", InsecureSkipVerify: true})
101 if err == nil {
102 conn2.Close()
103 t.Fatal("handshake for non-whitelisted host succeeded")
104 }
105}