oficina / home Public

Front door: no-store both branches, and cover the anonymous one

Review (Fable) found a real caching defect in the previous commit. "/"
now answers two different bodies keyed on a session cookie, but no-store
was set only on the signed-in branch and nothing emitted Vary. A shared
cache could store the anonymous front page and serve it to signed-in
people. Both headers are now set before either branch runs.

The session is read with sessions.Current, which uses what Middleware
already resolved for this request instead of querying the database a
second time.

Tests gained the assertions the anonymous branch never had: it sets no
cookie, it is no-store, and a cookie that does not resolve degrades to
the front page rather than to a redirect loop or a partial catalogue.

The colophon said "two fonts" while serving four files; it now says two
typefaces, which is what was meant and what is true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 708c80fbfba06c1d4586a0194afe3c958b3a1b95 parent 410adc4
3 files changed, +25 −3
  • internal/home/directory.go +11 −2
  • internal/home/directory_catalog_test.go +13 −0
  • internal/home/static/site/index.html +1 −1

CI passed — run details

diff --git a/internal/home/directory.go b/internal/home/directory.go
index 7b38318..fefa080 100644
--- a/internal/home/directory.go
+++ b/internal/home/directory.go
@@ -89,11 +89,20 @@ func (a *App) directoryRouter() http.Handler {
// The session is READ here, never required, so an expired cookie
// degrades to the front page rather than to a redirect loop.
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
- if _, ok := a.sessions.From(r); !ok {
+ // no-store covers BOTH branches, and it is set before either
+ // runs. One URL now answers two different bodies keyed on a
+ // cookie, so a shared cache that stored the anonymous 200
+ // would hand the front page to signed-in people. Vary: Cookie
+ // says the same thing to a cache that honours it; no-store is
+ // the half that does not depend on it honouring anything.
+ w.Header().Set("Cache-Control", "no-store")
+ w.Header().Add("Vary", "Cookie")
+ // Current reads what Middleware already resolved for this
+ // request, rather than asking the database a second time.
+ if _, ok := sessions.Current(r); !ok {
a.handleIndex(w, r)
return
}
- w.Header().Set("Cache-Control", "no-store")
a.render(w, 200, "directory.html", map[string]any{"Apps": directoryCatalog(*a.runtime)})
})
r.Post("/signout", func(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/home/directory_catalog_test.go b/internal/home/directory_catalog_test.go
index 02ed273..54a0682 100644
--- a/internal/home/directory_catalog_test.go
+++ b/internal/home/directory_catalog_test.go
@@ -1,6 +1,7 @@
package home
import (
+ "net/http"
"strings"
"testing"
)
@@ -25,6 +26,18 @@ func TestDirectoryCatalogRequiresSigninAndDoesNotInventLaunchDestinations(t *tes
t.Fatal("anonymous front page exposed catalogue", leak)
}
}
+ noStore(t, anonymous)
+ if len(anonymous.Result().Cookies()) != 0 {
+ t.Fatal("front page set a cookie on an anonymous request")
+ }
+ // A cookie that does not resolve must degrade to the front page, not to
+ // a redirect loop and not to a partial catalogue render.
+ tampered := f.request("GET", "/", "", "", &http.Cookie{Name: f.p.directory.sessions.CookieName(), Value: "not-a-token"})
+ if tampered.Code != 200 || !strings.Contains(tampered.Body.String(), "office suite") ||
+ strings.Contains(tampered.Body.String(), "Choose a team") {
+ t.Fatal("tampered cookie did not degrade to the front page", tampered.Code)
+ }
+
cookie := f.session(t, chooserPerson)
page := f.request("GET", "/", "", "", cookie)
if page.Code != 200 {
diff --git a/internal/home/static/site/index.html b/internal/home/static/site/index.html
index 0826a32..8eadc30 100644
--- a/internal/home/static/site/index.html
+++ b/internal/home/static/site/index.html
@@ -237,7 +237,7 @@
<li><a href="https://amadan.net/oficina/docs/-/releases/v0.1.0">Releases</a></li>
</ul>
</div>
- <p class="colophon">This page is one HTML file, one stylesheet and two fonts served
+ <p class="colophon">This page is one HTML file, one stylesheet and two typefaces served
from our own machines. It runs no scripts, sets no cookies and calls nothing
third-party &mdash; which is why there is no banner asking your permission.</p>
</div>