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}