package httpmw import ( "compress/gzip" "crypto/subtle" "net/http" "strings" ) const bearerPrefix = "Bearer " // Auth guards a handler with a constant-time bearer-token check. func Auth(token string, next http.Handler) http.Handler { want := []byte(token) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := r.Header.Get("Authorization") if !strings.HasPrefix(h, bearerPrefix) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } got := []byte(strings.TrimPrefix(h, bearerPrefix)) if subtle.ConstantTimeCompare(got, want) != 1 { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } // gzipWriter compresses the body and drops Content-Length, which no longer // describes what goes on the wire. WriteHeader is where the decision is made: // only then is Content-Type known. type gzipWriter struct { http.ResponseWriter gz *gzip.Writer decided bool } // compressible covers what this server actually serves in bulk: HTML, CSS, JS // and JSON. Fonts are woff2, which is already compressed — gzipping them costs // CPU to add bytes. func compressible(contentType string) bool { ct, _, _ := strings.Cut(contentType, ";") switch strings.TrimSpace(ct) { case "text/html", "text/css", "text/javascript", "application/javascript", "application/json", "text/plain": return true } return false } func (w *gzipWriter) WriteHeader(status int) { if !w.decided { w.decided = true if compressible(w.Header().Get("Content-Type")) { w.Header().Set("Content-Encoding", "gzip") w.Header().Del("Content-Length") w.gz = gzip.NewWriter(w.ResponseWriter) } } w.ResponseWriter.WriteHeader(status) } func (w *gzipWriter) Write(b []byte) (int, error) { if !w.decided { w.WriteHeader(http.StatusOK) } if w.gz != nil { return w.gz.Write(b) } return w.ResponseWriter.Write(b) } // Gzip compresses text responses for clients that ask. The templates, // stylesheet and htmx together are ~120 KB uncompressed and roughly a quarter // of that gzipped, which is the difference between a fast and a slow first load // on mobile data. func Gzip(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { next.ServeHTTP(w, r) return } w.Header().Add("Vary", "Accept-Encoding") gw := &gzipWriter{ResponseWriter: w} defer func() { if gw.gz != nil { gw.gz.Close() } }() next.ServeHTTP(gw, r) }) } // CORS reflects the request Origin only when it is in allowed, answers // preflight OPTIONS with 204, and passes everything else through. It wraps the // auth middleware so preflight (which carries no Authorization header) is never // rejected by auth. func CORS(allowed []string, next http.Handler) http.Handler { set := make(map[string]struct{}, len(allowed)) for _, o := range allowed { set[o] = struct{}{} } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if _, ok := set[origin]; ok && origin != "" { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Add("Vary", "Origin") w.Header().Set("Access-Control-Allow-Methods", "GET,PUT,DELETE,OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type") w.Header().Set("Access-Control-Max-Age", "86400") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }