oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "testing"
7
8 "amadan.net/oficina/estilo.git/sky"
9 "amadan.net/oficina/estilo.git/theme"
10 "github.com/go-chi/chi/v5"
11)
12
13// sharedPublicPaths are served on BOTH Home origins: the design tokens, the
14// brand marks, Calma's font and sky, and the front page's stylesheet and fonts. They are listed
15// once because two copies of this list would drift, and a route that
16// quietly stopped being shared — or quietly started — is exactly what the
17// two tests below exist to catch.
18var sharedPublicPaths = []string{
19 "/healthz", "/static/tokens.css", "/static/theme.css",
20 "/static/brand/icon.svg", "/static/brand/icon-32.png", "/static/brand/apple-touch-icon.png", "/static/brand/brand.css", "/static/brand/LICENSE.lucide", "/static/brand/docs.svg", "/static/brand/sheets.svg", "/static/brand/slides.svg", "/static/brand/calendar.svg", "/static/brand/calls.svg", "/static/brand/memoria.svg",
21 "/static/brand/fichas.svg", "/static/brand/mail.svg", "/static/brand/files.svg",
22 // Calma: Onest, the sky and its stylesheet, and Home's rules for placing it.
23 "/static/estilo/fonts/v1/*", "/static/estilo/sky/v1/*", "/static/estilo/sky.css", "/static/calma.css",
24 "/static/busy.css", "/static/busy.js",
25 "/static/site/home.css", "/static/site/apps.css",
26 // estilo's shared chrome: base links, menus, the bar and the door.
27 "/static/estilo/base.css", "/static/estilo/menu.css", "/static/estilo/chrome.css",
28}
29
30// examplePath is a real file under a wildcard route, so fetching it
31// proves the handler is mounted rather than merely that chi has a pattern.
32func examplePath(route string) string {
33 switch route {
34 case "/static/estilo/fonts/v1/*":
35 return theme.FontPrefix + "onest-latin.woff2"
36 case "/static/estilo/sky/v1/*":
37 for _, layer := range sky.For(sky.SuiteSeed).Layers {
38 if layer.Src != "" { // the placeholder is inline, with no URL
39 return layer.Src
40 }
41 }
42 }
43 return route
44}
45
46func sharedPublic(route string) bool {
47 for _, p := range sharedPublicPaths {
48 if p == route {
49 return true
50 }
51 }
52 return false
53}
54
55func TestEveryCredentialRouteRefusesDirectoryHost(t *testing.T) {
56 app := newTestApp(t)
57 router := app.Router()
58 routes := router.(chi.Routes)
59 count := 0
60 err := chi.Walk(routes, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
61 // "/" is the front page, which both origins answer (the credential
62 // host redirects rather than rendering — see the test below).
63 switch {
64 case route == "/", route == "/help/open-source", route == "/static/credits.css", sharedPublic(route):
65 return nil
66 }
67 count++
68 req := httptest.NewRequest(method, "https://"+directoryHost+route, nil)
69 req.Header.Set("Origin", Origin)
70 rec := httptest.NewRecorder()
71 router.ServeHTTP(rec, req)
72 if rec.Code != http.StatusNotFound || len(rec.Result().Cookies()) != 0 || rec.Body.Len() != 0 {
73 t.Errorf("%s %s on directory: status=%d cookies=%d body=%q", method, route, rec.Code, len(rec.Result().Cookies()), rec.Body.String())
74 }
75 return nil
76 })
77 if err != nil || count < 19 {
78 t.Fatalf("route walk err=%v credential routes=%d", err, count)
79 }
80 if _, ok := app.mailer.last(); ok {
81 t.Fatal("directory request sent mail")
82 }
83}
84
85func TestDirectoryContentNeverServedOnCredentialHost(t *testing.T) {
86 app := newTestApp(t)
87 req := httptest.NewRequest(http.MethodGet, Origin+"/?team=untrusted", nil)
88 rec := httptest.NewRecorder()
89 app.Router().ServeHTTP(rec, req)
90 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != directoryOrigin+"/" || rec.Body.Len() != 0 {
91 t.Fatalf("credential root: status=%d location=%q body=%q", rec.Code, rec.Header().Get("Location"), rec.Body.String())
92 }
93}
94
95func TestUnknownHostsCannotReachRoutes(t *testing.T) {
96 app := newTestApp(t)
97 router := app.Router()
98 hosts := []string{"", "example.com", "oficina.works.attacker.invalid", "id.oficina.works.attacker.invalid", "id.oficina.works.", "id.oficina.works:443", "oficina.works:443", "localhost"}
99 err := chi.Walk(router.(chi.Routes), func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
100 for _, host := range hosts {
101 req := httptest.NewRequest(method, route, nil)
102 req.Host = host
103 req.Header.Set("Origin", Origin)
104 req.Header.Set("X-Forwarded-Host", credentialHost)
105 req.Header.Set("Forwarded", "host="+credentialHost)
106 rec := httptest.NewRecorder()
107 router.ServeHTTP(rec, req)
108 if rec.Code != http.StatusNotFound || len(rec.Result().Cookies()) != 0 || rec.Body.Len() != 0 {
109 t.Errorf("%s %s host=%q status=%d", method, route, host, rec.Code)
110 }
111 }
112 return nil
113 })
114 if err != nil {
115 t.Fatal(err)
116 }
117}
118
119func TestDirectoryIsNotWithinCredentialRP(t *testing.T) {
120 app := newTestApp(t)
121 if credentialHost == directoryHost || app.credentialHost == directoryHost || Origin == directoryOrigin {
122 t.Fatal("directory overlaps the credential origin or relying party")
123 }
124}
125
126func TestSharedRoutesOnBothOrigins(t *testing.T) {
127 app := newTestApp(t)
128 for _, host := range []string{credentialHost, directoryHost} {
129 for _, path := range sharedPublicPaths {
130 path = examplePath(path)
131 req := httptest.NewRequest(http.MethodGet, "https://"+host+path, nil)
132 rec := httptest.NewRecorder()
133 app.Router().ServeHTTP(rec, req)
134 if rec.Code != http.StatusOK || len(rec.Result().Cookies()) != 0 {
135 t.Errorf("host=%s path=%s status=%d cookies=%d", host, path, rec.Code, len(rec.Result().Cookies()))
136 }
137 }
138 }
139}
140
141func TestDirectoryCredentialPostRefusedBeforeCSRF(t *testing.T) {
142 app := newTestApp(t)
143 req := httptest.NewRequest(http.MethodPost, directoryOrigin+"/signin", nil)
144 rec := httptest.NewRecorder()
145 app.Router().ServeHTTP(rec, req)
146 if rec.Code != http.StatusNotFound || rec.Body.Len() != 0 {
147 t.Fatalf("directory credential POST status=%d body=%q", rec.Code, rec.Body.String())
148 }
149}
150