e2e/acme_test.go

9df917e73a67d15adecc3f45976690f6fcd4e47a
gitbay/e2e/acme_test.go history · blame · raw

107 lines · 3320 bytes

  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	t.Parallel()
 22	inst := startInstanceWith(t, "") // helper for binary + keys; killed below
 23	inst.proc.Process.Kill()
 24	inst.proc.Wait()
 25
 26	ports := freePorts(t, 2)
 27	httpsPort, acmeHTTPPort := ports[0], ports[1]
 28	cfg := fmt.Sprintf(`
 29[server]
 30root = %q
 31site_url = "https://gitbay.example"
 32secret_key_file = %q
 33[ssh]
 34port = %d
 35[http]
 36addr = "127.0.0.1:%d"
 37tls = "acme"
 38acme_email = "noreply@gitbay.example"
 39acme_http_addr = "127.0.0.1:%d"
 40`, inst.root, inst.keyFile, inst.port, httpsPort, acmeHTTPPort)
 41	if err := os.WriteFile(inst.config, []byte(cfg), 0o600); err != nil {
 42		t.Fatal(err)
 43	}
 44	inst.proc = exec.Command(inst.gitbayd, "--config", inst.config, "serve")
 45	inst.proc.Stderr = os.Stderr
 46	if err := inst.proc.Start(); err != nil {
 47		t.Fatal(err)
 48	}
 49	t.Cleanup(func() { inst.proc.Process.Kill(); inst.proc.Wait() })
 50
 51	wait := func(port int) {
 52		t.Helper()
 53		deadline := time.Now().Add(10 * time.Second)
 54		for {
 55			conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 200*time.Millisecond)
 56			if err == nil {
 57				conn.Close()
 58				return
 59			}
 60			if time.Now().After(deadline) {
 61				t.Fatalf("port %d never came up", port)
 62			}
 63			time.Sleep(50 * time.Millisecond)
 64		}
 65	}
 66	wait(httpsPort)
 67	wait(acmeHTTPPort)
 68
 69	// The helper listener redirects everything to the canonical HTTPS host.
 70	client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
 71		return http.ErrUseLastResponse
 72	}}
 73	resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/repo/log?x=1", acmeHTTPPort))
 74	if err != nil {
 75		t.Fatal(err)
 76	}
 77	resp.Body.Close()
 78	if resp.StatusCode != http.StatusMovedPermanently ||
 79		resp.Header.Get("Location") != "https://gitbay.example/alice/repo/log?x=1" {
 80		t.Fatalf("redirect: %d %q", resp.StatusCode, resp.Header.Get("Location"))
 81	}
 82
 83	// A TLS handshake reaches autocert, which tries (and fails) to issue —
 84	// the handshake errors, the daemon survives, the listener stays up.
 85	conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp",
 86		fmt.Sprintf("127.0.0.1:%d", httpsPort),
 87		&tls.Config{ServerName: "gitbay.example", InsecureSkipVerify: true})
 88	if err == nil {
 89		conn.Close()
 90		t.Fatal("handshake unexpectedly succeeded with no CA reachable")
 91	}
 92	wait(httpsPort) // still listening after the failed handshake
 93
 94	// Certificates cache under the server root.
 95	if _, err := os.Stat(filepath.Join(inst.root, "acme")); err != nil {
 96		t.Fatalf("acme cache dir: %v", err)
 97	}
 98
 99	// A host outside the whitelist is refused before any issuance attempt.
100	conn2, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp",
101		fmt.Sprintf("127.0.0.1:%d", httpsPort),
102		&tls.Config{ServerName: "evil.example", InsecureSkipVerify: true})
103	if err == nil {
104		conn2.Close()
105		t.Fatal("handshake for non-whitelisted host succeeded")
106	}
107}