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) }, )) }