The template's files at a77fd30, without its history or LICENSE, after script/rename simplexcalc. Model: opus-5-5
88 lines
2.9 KiB
Go
88 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"sneak.berlin/go/simplexcalc/static"
|
|
)
|
|
|
|
// staticPrefix is where the embedded assets are mounted.
|
|
const staticPrefix = "/static/"
|
|
|
|
// staticCacheControl is a year, because the asset set is baked into the
|
|
// binary: a new build is a new deployment, and a deployment is the only
|
|
// thing that can change these bytes. Bump the path if that stops being
|
|
// true.
|
|
const staticCacheControl = "public, max-age=31536000, immutable"
|
|
|
|
// SetupRoutes builds the router. The middleware order is the contract
|
|
// of this file, and it is this way for reasons:
|
|
//
|
|
// 1. RequestID first, so every later line of log and every captured
|
|
// error can name the request it came from.
|
|
// 2. Recoverer next-to-outermost, so it covers every handler and every
|
|
// middleware below it. Above the logger, so a panic still produces
|
|
// a logged request line.
|
|
// 3. RequestLogger and Metrics before the work, so both see the final
|
|
// status of every request including the 500 the recoverer wrote.
|
|
// 4. SecurityHeaders before anything can write a body — the headers
|
|
// have to be set before the first Write, and a 404 or a panic
|
|
// response needs them as much as a page does.
|
|
// 5. Timeout and BodyLimit before any handler reads a body.
|
|
// 6. CSRF innermost of the global chain, wrapping only the routes that
|
|
// can change state.
|
|
//
|
|
// /metrics and the healthcheck sit outside CSRF (neither is
|
|
// state-changing, and a scraper has no token) and outside nothing else.
|
|
func (s *Server) SetupRoutes() {
|
|
r := chi.NewRouter()
|
|
|
|
r.Use(s.mw.RequestID())
|
|
r.Use(s.mw.Recoverer())
|
|
r.Use(s.mw.RequestLogger())
|
|
r.Use(s.mw.Metrics())
|
|
r.Use(s.mw.SecurityHeaders())
|
|
r.Use(s.mw.Timeout())
|
|
r.Use(s.mw.BodyLimit())
|
|
|
|
r.NotFound(s.h.NotFound())
|
|
r.MethodNotAllowed(s.h.MethodNotAllowed())
|
|
|
|
// Operational endpoints: no CSRF, no session, no HTML.
|
|
r.Get("/.well-known/healthcheck.json", s.h.Healthcheck())
|
|
r.Method(http.MethodGet, "/metrics", s.metrics.Handler())
|
|
|
|
r.Handle(staticPrefix+"*", s.staticHandler())
|
|
|
|
// Everything a browser drives, behind CSRF. Safe methods are
|
|
// unaffected by it except that they are issued a token.
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(s.mw.CSRF())
|
|
|
|
r.Get("/", s.h.Index())
|
|
r.Post("/widgets", s.h.CreateWidget())
|
|
|
|
if s.params.Config.Debug {
|
|
// Only with DEBUG=true: a route that panics on demand is a
|
|
// denial-of-service primitive in production.
|
|
r.Get("/debug/panic", s.h.Panic())
|
|
}
|
|
})
|
|
|
|
s.router = r
|
|
}
|
|
|
|
// staticHandler serves the embedded assets, without directory listings
|
|
// and with a long cache lifetime.
|
|
func (s *Server) staticHandler() http.Handler {
|
|
fileServer := http.FileServer(filesOnly{inner: http.FS(static.FS)})
|
|
|
|
return http.StripPrefix(staticPrefix, http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Cache-Control", staticCacheControl)
|
|
fileServer.ServeHTTP(w, r)
|
|
},
|
|
))
|
|
}
|