Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 58m ago 32290114
history blame raw 6.0 KiB
1package server
2
3import (
4 "context"
5 "crypto/tls"
6 "net/http"
7 "net/http/httptest"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "golang.org/x/crypto/acme"
14 "rodent.software/vole/forge/config"
15)
16
17func testACMEConfig(t *testing.T) *config.Config {
18 t.Helper()
19 return &config.Config{
20 DataDir: t.TempDir(),
21 HTTPAddr: "127.0.0.1:8080",
22 HTTPSAddr: "127.0.0.1:8443",
23 PublicURL: "https://Forge.Example.com",
24 }
25}
26
27func TestACMEDerivesTheHostFromPublicURL(t *testing.T) {
28 t.Parallel()
29 cfg := testACMEConfig(t)
30 a, err := newACME(cfg, nil)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if got := a.hostNames(); len(got) != 1 || got[0] != "forge.example.com" {
35 t.Fatalf("hosts = %v, want [forge.example.com]", got)
36 }
37 if _, err := os.Stat(cfg.ACMECachePath()); err != nil {
38 t.Fatalf("cache directory was not created: %v", err)
39 }
40 found := false
41 for _, proto := range a.tlsConfig().NextProtos {
42 if proto == acme.ALPNProto {
43 found = true
44 }
45 }
46 if !found {
47 t.Fatalf("TLS config does not offer %q: %v", acme.ALPNProto, a.tlsConfig().NextProtos)
48 }
49 if a.tlsConfig().MinVersion != tls.VersionTLS12 {
50 t.Fatalf("MinVersion = %x, want TLS 1.2", a.tlsConfig().MinVersion)
51 }
52}
53
54// A host from acme_domains is enough on its own; public_url is optional and may
55// be left for another day.
56func TestACMEAcceptsExplicitDomainsWithoutPublicURL(t *testing.T) {
57 t.Parallel()
58 cfg := testACMEConfig(t)
59 cfg.PublicURL = ""
60 cfg.ACMEDomains = []string{"git.example.com", "Forge.Example.com."}
61 a, err := newACME(cfg, nil)
62 if err != nil {
63 t.Fatal(err)
64 }
65 if got := a.hostNames(); strings.Join(got, ",") != "forge.example.com,git.example.com" {
66 t.Fatalf("hosts = %v", got)
67 }
68 for _, host := range []string{"forge.example.com", "git.example.com"} {
69 if err := a.allows(context.Background(), host); err != nil {
70 t.Fatalf("allows(%q) = %v", host, err)
71 }
72 }
73 if err := a.allows(context.Background(), "other.example.com"); err == nil {
74 t.Fatal("an unlisted host was allowed")
75 }
76}
77
78// A request must be able to redirect to the configured HTTPS address, including
79// the port when it is not 443. The unknown-host case is 404 rather than a
80// redirect that reflects an attacker's Host header.
81func TestACMEHTTPRedirectsOnlyKnownHosts(t *testing.T) {
82 t.Parallel()
83 cfg := testACMEConfig(t)
84 a, err := newACME(cfg, nil)
85 if err != nil {
86 t.Fatal(err)
87 }
88 h := a.httpHandler()
89
90 req := httptest.NewRequest(http.MethodGet, "http://forge.example.com/demo?tab=code", nil)
91 rec := httptest.NewRecorder()
92 h.ServeHTTP(rec, req)
93 if rec.Code != http.StatusMovedPermanently {
94 t.Fatalf("status = %d, want 301", rec.Code)
95 }
96 if got, want := rec.Header().Get("Location"), "https://forge.example.com:8443/demo?tab=code"; got != want {
97 t.Fatalf("Location = %q, want %q", got, want)
98 }
99
100 req = httptest.NewRequest(http.MethodGet, "http://elsewhere.example.com/", nil)
101 rec = httptest.NewRecorder()
102 h.ServeHTTP(rec, req)
103 if rec.Code != http.StatusNotFound {
104 t.Fatalf("unknown host status = %d, want 404", rec.Code)
105 }
106 if loc := rec.Header().Get("Location"); loc != "" {
107 t.Fatalf("unknown host redirected to %q", loc)
108 }
109}
110
111// The default HTTPS port is omitted from a redirect so the URL is the canonical
112// one people expect.
113func TestACMEHTTPRedirectOmitsTheDefaultPort(t *testing.T) {
114 t.Parallel()
115 cfg := testACMEConfig(t)
116 cfg.HTTPSAddr = ":443"
117 a, err := newACME(cfg, nil)
118 if err != nil {
119 t.Fatal(err)
120 }
121 req := httptest.NewRequest(http.MethodHead, "http://forge.example.com/status", nil)
122 rec := httptest.NewRecorder()
123 a.httpHandler().ServeHTTP(rec, req)
124 if got, want := rec.Header().Get("Location"), "https://forge.example.com/status"; got != want {
125 t.Fatalf("Location = %q, want %q", got, want)
126 }
127}
128
129// Site hosts are certificates on demand, but only for repositories that are
130// publicly readable. Otherwise any Host header could make the certificate
131// authority issue a name, which is both a cache of junk and a route to a rate
132// limit, and a private repository's name would reach a public certificate log.
133func TestACMESiteCertificatesRequireAPublicRepository(t *testing.T) {
134 t.Parallel()
135 cfg := testACMEConfig(t)
136 cfg.PagesDomain = "pages.example.com"
137 a, err := newACME(cfg, func(name string) bool { return name == "demo" })
138 if err != nil {
139 t.Fatal(err)
140 }
141 for host, want := range map[string]bool{
142 "demo.pages.example.com": true,
143 "demo.pages.example.com:443": true, // HostPolicy is called without a port, but normalize anyway
144 "missing.pages.example.com": false,
145 "deep.demo.pages.example.com": false,
146 "pages.example.com": false,
147 "forge.example.com": true,
148 } {
149 err := a.allows(context.Background(), host)
150 if (err == nil) != want {
151 t.Errorf("allows(%q) = %v, want allowed=%v", host, err, want)
152 }
153 }
154}
155
156func TestACMERejectsInvalidCertificateHosts(t *testing.T) {
157 t.Parallel()
158 for _, raw := range []string{"", "forge", "*.example.com", "https://example.com", "example.com:443", "127.0.0.1", "-forge.example.com", "for ge.example.com"} {
159 if host, err := certificateHost(raw); err == nil {
160 t.Errorf("certificateHost(%q) = %q, want an error", raw, host)
161 }
162 }
163 if host, err := certificateHost("Forge.Example.com."); err != nil || host != "forge.example.com" {
164 t.Fatalf("certificateHost = %q, %v", host, err)
165 }
166}
167
168func TestACMERequiresAnHTTPsPublicURL(t *testing.T) {
169 t.Parallel()
170 cfg := testACMEConfig(t)
171 cfg.PublicURL = "http://forge.example.com"
172 if _, err := newACME(cfg, nil); err == nil {
173 t.Fatal("an http public_url was accepted with https_addr")
174 }
175
176 cfg = testACMEConfig(t)
177 cfg.PublicURL = ""
178 if _, err := newACME(cfg, nil); err == nil {
179 t.Fatal("https_addr without a certificate host was accepted")
180 }
181}
182
183func TestACMEDefaultsAndOverridesTheCacheDirectory(t *testing.T) {
184 t.Parallel()
185 cfg := testACMEConfig(t)
186 if got, want := cfg.ACMECachePath(), filepath.Join(cfg.DataDir, "acme"); got != want {
187 t.Fatalf("default cache = %q, want %q", got, want)
188 }
189 cfg.ACMECacheDir = filepath.Join(t.TempDir(), "certs")
190 if got := cfg.ACMECachePath(); got != cfg.ACMECacheDir {
191 t.Fatalf("cache = %q, want %q", got, cfg.ACMECacheDir)
192 }
193}
194