Seed from go-template-repo, renamed to simplexcalc

The template's files at a77fd30, without its history or LICENSE, after
script/rename simplexcalc.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 21:38:57 +00:00
parent e336f46e34
commit f8ce8cef83
79 changed files with 7131 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
package middleware
import (
"net/http"
"strconv"
)
// BodyLimit caps how much of a request body a handler can read.
//
// http.MaxBytesReader is the mechanism, and the reason to use it rather
// than checking Content-Length is that Content-Length is a claim: a
// chunked request does not send one, and a lying one is trivial to
// send. MaxBytesReader counts the bytes that actually arrive and makes
// the read fail past the cap, so the ceiling holds whatever the headers
// said.
//
// It also sets the response's error status itself (413) when the limit
// is hit during a read, so a handler that ignores the read error still
// cannot serve a success off a truncated body.
//
// Content-Length is still checked first, as an early refusal: it costs
// nothing and it lets an oversized upload be rejected before it is
// transferred.
func (m *Middleware) BodyLimit() func(http.Handler) http.Handler {
limit := m.cfg.MaxRequestBody
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength > limit {
w.Header().Set("Content-Length", strconv.Itoa(len(tooLargeBody)))
http.Error(w, tooLargeBody, http.StatusRequestEntityTooLarge)
return
}
r.Body = http.MaxBytesReader(w, r.Body, limit)
next.ServeHTTP(w, r)
})
}
}
// tooLargeBody is the response to an oversized request. It names no
// limit: the number is an operational detail and telling a caller
// exactly where the ceiling is only helps them sit under it.
const tooLargeBody = "request body too large"
+116
View File
@@ -0,0 +1,116 @@
package middleware
import (
"crypto/rand"
"html/template"
"net/http"
"strings"
"github.com/gorilla/csrf"
)
// csrfCookieName is deliberately not the library default: a name that
// says which service issued it makes a cookie jar readable, and two
// services on sibling hosts do not fight over one name.
const csrfCookieName = "simplexcalc_csrf"
// csrfMaxAge bounds how long a token stays valid, in seconds.
const csrfMaxAge = 12 * 60 * 60
// csrfKeyBytes is the key length gorilla/csrf requires.
const csrfKeyBytes = 32
// CSRF protects state-changing routes (POST, PUT, PATCH, DELETE). Safe
// methods pass through and are issued a token.
//
// The key comes from config: CSRF_KEY when set, otherwise a random key
// generated here and logged as such. An ephemeral key is correct for
// development and wrong for anything with more than one replica or more
// than one process lifetime, because a token issued by one key is
// rejected by another — the user sees a failed form submission, not a
// security event. That is why it is a warning at startup and a
// documented configuration key rather than a silent default.
func (m *Middleware) CSRF() func(http.Handler) http.Handler {
key := m.cfg.CSRFKey
if m.cfg.CSRFKeyEphemeral {
key = make([]byte, csrfKeyBytes)
// crypto/rand.Read cannot fail on any supported platform; it
// panics internally rather than returning an error a caller
// might ignore. A key that is not random is not a key, so
// there is nothing to fall back to here anyway.
_, _ = rand.Read(key)
m.log.Warn("CSRF_KEY is not set; using a random key for this process",
"consequence", "tokens do not survive a restart and are not shared between replicas")
}
protect := csrf.Protect(
key,
// Secure cookies require TLS, which is absent in local
// development; tying the flag to the same switch that governs
// HSTS keeps "is this a production deployment" a single
// decision rather than two that can disagree.
csrf.Secure(m.cfg.HSTS),
csrf.HttpOnly(true),
csrf.SameSite(csrf.SameSiteLaxMode),
csrf.Path("/"),
csrf.CookieName(csrfCookieName),
csrf.MaxAge(csrfMaxAge),
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.log.Warn("csrf rejection",
"id", RequestIDFrom(r.Context()),
"path", r.URL.Path,
"reason", csrf.FailureReason(r).Error(),
)
http.Error(w, "invalid CSRF token", http.StatusForbidden)
})),
)
// markScheme must be OUTSIDE protect: it sets a context value that
// protect reads, so it has to run first.
return func(next http.Handler) http.Handler {
return markScheme(protect(next))
}
}
// markScheme tells gorilla/csrf whether the browser's connection was
// plaintext, because the library cannot tell and assumes it was not.
//
// Its strict Referer check is for TLS only, and it treats every request
// as TLS unless a context value says otherwise. A service behind a
// TLS-terminating reverse proxy receives plaintext HTTP with an
// https:// Referer — the library then applies the TLS rules to a
// plaintext connection and rejects every form submission, which is a
// total outage of every state-changing route rather than a subtle bug.
// Left alone, the same misreading rejects plain HTTP in development for
// the mirror-image reason.
//
// The rule: HTTPS if the connection is TLS, or if a proxy said so with
// X-Forwarded-Proto. Trusting that header is safe in this one
// direction — the only thing an attacker gains by setting it is
// STRICTER checking of their own request. The reverse (inferring
// plaintext) is what would weaken the check, and nothing a client sends
// can cause it.
//
// A deployment behind a proxy that does not set X-Forwarded-Proto gets
// the plaintext ruleset: tokens still work, and the extra Referer check
// TLS would have added is not applied. Configure the proxy.
func markScheme(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && !strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
r = csrf.PlaintextHTTPRequest(r)
}
next.ServeHTTP(w, r)
})
}
// CSRFField returns the hidden input for r's token, for a template to
// place inside a form. Handlers call this rather than importing
// gorilla/csrf, so the library stays swappable behind this package.
func CSRFField(r *http.Request) template.HTML {
return csrf.TemplateField(r)
}
+163
View File
@@ -0,0 +1,163 @@
// Package middleware holds the HTTP middleware chain: request
// identity, logging, metrics, panic recovery, timeouts, body caps,
// security headers and CSRF.
//
// Order matters and is fixed in internal/server/routes.go, not here.
package middleware
import (
"context"
"log/slog"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"go.uber.org/fx"
"sneak.berlin/go/simplexcalc/internal/config"
"sneak.berlin/go/simplexcalc/internal/logger"
"sneak.berlin/go/simplexcalc/internal/telemetry"
)
// contextKey is this package's private context key type, so no other
// package can collide with or read these values by accident.
type contextKey string
// requestIDKey carries the per-request id.
const requestIDKey contextKey = "request-id"
// RequestIDHeader is the response header the id is echoed in, so a
// user reporting a failure can quote something that finds the log line.
const RequestIDHeader = "X-Request-Id"
// Params defines dependencies for Middleware.
type Params struct {
fx.In
Config *config.Config
Logger *logger.Logger
Sentry *telemetry.Sentry
Metrics *telemetry.Metrics
}
// Middleware is the set of handlers, built once and reused.
type Middleware struct {
params Params
log *slog.Logger
cfg *config.Config
}
// New creates the middleware set.
//
//nolint:revive // lc parameter is required by fx even if unused.
func New(lc fx.Lifecycle, params Params) (*Middleware, error) {
return &Middleware{
params: params,
log: params.Logger.Get(),
cfg: params.Config,
}, nil
}
// RequestIDFrom returns the id assigned to r's context, or "" outside a
// request that went through RequestID.
func RequestIDFrom(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}
// RequestID assigns each request an id and echoes it. An id supplied by
// the client is ignored: it is attacker-controlled, it would let a
// caller collide two unrelated requests in the log, and there is no
// trusted proxy contract here that would make it meaningful.
func (m *Middleware) RequestID() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.NewString()
w.Header().Set(RequestIDHeader, id)
next.ServeHTTP(w, r.WithContext(
context.WithValue(r.Context(), requestIDKey, id),
))
})
}
}
// RequestLogger logs one line per completed request.
func (m *Middleware) RequestLogger() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := newResponseRecorder(w)
next.ServeHTTP(rec, r)
m.log.Info("request",
"id", RequestIDFrom(r.Context()),
"method", r.Method,
"path", r.URL.Path,
"route", routePattern(r),
"status", rec.Status(),
"bytes", rec.written,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
}
// Metrics records the Prometheus series for each request.
func (m *Middleware) Metrics() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := newResponseRecorder(w)
m.params.Metrics.InFlightAdd(1)
defer m.params.Metrics.InFlightAdd(-1)
next.ServeHTTP(rec, r)
m.params.Metrics.Observe(
r.Method,
routePattern(r),
strconv.Itoa(rec.Status()),
time.Since(start),
)
})
}
}
// Timeout bounds handler execution with the configured request timeout.
// The handler sees a context with a deadline; a handler that ignores it
// still runs to completion, so handlers must pass the context down to
// everything that can block.
func (m *Middleware) Timeout() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), m.cfg.RequestTimeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// routePattern returns the chi route pattern for r, or "unmatched" when
// no route matched (a 404). It is what the metrics and the log are
// labelled by; see the comment on the requests counter for why the path
// is not.
func routePattern(r *http.Request) string {
rctx := chi.RouteContext(r.Context())
if rctx == nil {
return "unmatched"
}
pattern := rctx.RoutePattern()
if pattern == "" {
return "unmatched"
}
return pattern
}
+314
View File
@@ -0,0 +1,314 @@
package middleware_test
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"sneak.berlin/go/simplexcalc/internal/config"
"sneak.berlin/go/simplexcalc/internal/globals"
"sneak.berlin/go/simplexcalc/internal/logger"
"sneak.berlin/go/simplexcalc/internal/middleware"
"sneak.berlin/go/simplexcalc/internal/telemetry"
)
// newMiddleware builds the set against a given config, with logging
// discarded and telemetry disabled.
func newMiddleware(t *testing.T, cfg *config.Config) *middleware.Middleware {
t.Helper()
g := &globals.Globals{Appname: "simplexcalc", Version: "test"}
log, err := logger.New(nil, logger.Params{Globals: g, Output: io.Discard})
if err != nil {
t.Fatalf("building logger: %v", err)
}
sentry, err := telemetry.NewSentry(nil, telemetry.SentryParams{
Config: cfg, Globals: g, Logger: log,
})
if err != nil {
t.Fatalf("building sentry: %v", err)
}
metrics, err := telemetry.NewMetrics(telemetry.MetricsParams{Config: cfg})
if err != nil {
t.Fatalf("building metrics: %v", err)
}
mw, err := middleware.New(nil, middleware.Params{
Config: cfg, Logger: log, Sentry: sentry, Metrics: metrics,
})
if err != nil {
t.Fatalf("building middleware: %v", err)
}
return mw
}
// getReq and postReq build requests carrying the test's context, so a
// handler that respects cancellation is exercised the way the server
// exercises it.
func getReq(t *testing.T) *http.Request {
t.Helper()
return httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
}
func postReq(t *testing.T, body string) *http.Request {
t.Helper()
return httptest.NewRequestWithContext(
t.Context(), http.MethodPost, "/", strings.NewReader(body),
)
}
func testConfig() *config.Config {
return &config.Config{
Port: 8080,
HSTS: true,
MaxRequestBody: 1024,
RequestTimeout: time.Second,
ShutdownGrace: time.Second,
CSRFKeyEphemeral: true,
}
}
// TestRecovererAnswers500 is the point of the panic middleware: net/http
// on its own drops the connection, which tells the client nothing about
// whose fault it was.
func TestRecovererAnswers500(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
h := mw.Recoverer()(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
panic("boom")
}))
w := httptest.NewRecorder()
h.ServeHTTP(w, getReq(t))
if w.Code != http.StatusInternalServerError {
t.Fatalf("status = %d, want 500", w.Code)
}
if w.Body.Len() == 0 {
t.Error("a 500 with no body tells the client nothing")
}
// The panic value must not reach the client.
if strings.Contains(w.Body.String(), "boom") {
t.Error("the panic value was leaked in the response body")
}
}
// TestRecovererPassesThroughSuccess: the recovery wrapper must be
// invisible when nothing goes wrong, including for the response body.
func TestRecovererPassesThroughSuccess(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
h := mw.Recoverer()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusTeapot)
_, _ = w.Write([]byte("fine"))
}))
w := httptest.NewRecorder()
h.ServeHTTP(w, getReq(t))
if w.Code != http.StatusTeapot || w.Body.String() != "fine" {
t.Errorf("status = %d body = %q", w.Code, w.Body.String())
}
}
// TestSecurityHeadersOnEveryResponse, including responses the handler
// never got to write.
func TestSecurityHeadersOnEveryResponse(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
notFound := func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "not found", http.StatusNotFound)
}
h := mw.SecurityHeaders()(http.HandlerFunc(notFound))
w := httptest.NewRecorder()
h.ServeHTTP(w, getReq(t))
want := map[string]string{
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
}
for header, value := range want {
if got := w.Header().Get(header); got != value {
t.Errorf("%s = %q, want %q", header, got, value)
}
}
csp := w.Header().Get("Content-Security-Policy")
if !strings.Contains(csp, "default-src 'self'") {
t.Errorf("CSP = %q", csp)
}
if strings.Contains(csp, "unsafe-inline") {
t.Error("the CSP permits inline script or style")
}
}
// TestHSTSOffWhenDisabled: the header must be absent, not empty, so a
// developer's browser is never pinned to HTTPS on localhost.
func TestHSTSOffWhenDisabled(t *testing.T) {
t.Parallel()
cfg := testConfig()
cfg.HSTS = false
mw := newMiddleware(t, cfg)
noop := func(_ http.ResponseWriter, _ *http.Request) {}
h := mw.SecurityHeaders()(http.HandlerFunc(noop))
w := httptest.NewRecorder()
h.ServeHTTP(w, getReq(t))
if _, ok := w.Header()["Strict-Transport-Security"]; ok {
t.Error("HSTS was sent with HSTS disabled")
}
}
// TestBodyLimitRefusesDeclaredOversize: a Content-Length over the cap is
// refused before the body transfers.
func TestBodyLimitRefusesDeclaredOversize(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
reached := false
h := mw.BodyLimit()(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
reached = true
}))
req := postReq(t, strings.Repeat("x", 2048))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusRequestEntityTooLarge {
t.Errorf("status = %d, want 413", w.Code)
}
if reached {
t.Error("the handler ran for an oversized request")
}
}
// TestBodyLimitCapsUndeclaredBody is the case Content-Length cannot
// catch: a body that arrives without one, or with a lying one, must
// still fail at the cap rather than being read in full.
func TestBodyLimitCapsUndeclaredBody(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
var readErr error
h := mw.BodyLimit()(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
_, readErr = io.ReadAll(r.Body)
}))
req := postReq(t, strings.Repeat("x", 4096))
// Undeclared length: what a chunked upload looks like here.
req.ContentLength = -1
h.ServeHTTP(httptest.NewRecorder(), req)
if readErr == nil {
t.Error("reading past the cap succeeded; the limit is not enforced on the read")
}
}
// TestBodyLimitAllowsNormalRequests, so the cap is not just "refuse
// everything".
func TestBodyLimitAllowsNormalRequests(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
got := ""
h := mw.BodyLimit()(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
got = string(b)
}))
req := postReq(t, "small")
h.ServeHTTP(httptest.NewRecorder(), req)
if got != "small" {
t.Errorf("body = %q, want %q", got, "small")
}
}
// TestRequestIDIsAssignedAndNotBorrowed: the id must be this process's,
// so a client cannot collide two unrelated requests in the log.
func TestRequestIDIsAssignedAndNotBorrowed(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
var inHandler string
h := mw.RequestID()(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
inHandler = middleware.RequestIDFrom(r.Context())
}))
req := getReq(t)
req.Header.Set(middleware.RequestIDHeader, "client-supplied")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if inHandler == "" {
t.Fatal("no request id reached the handler")
}
if inHandler == "client-supplied" {
t.Error("the client's request id was trusted")
}
if w.Header().Get(middleware.RequestIDHeader) != inHandler {
t.Error("the response header does not carry the id the handler saw")
}
}
// TestTimeoutGivesHandlerADeadline. The handler is what has to respect
// it, so what is asserted here is that the deadline is there at all.
func TestTimeoutGivesHandlerADeadline(t *testing.T) {
t.Parallel()
mw := newMiddleware(t, testConfig())
var hasDeadline bool
h := mw.Timeout()(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
_, hasDeadline = r.Context().Deadline()
}))
h.ServeHTTP(httptest.NewRecorder(), getReq(t))
if !hasDeadline {
t.Error("the handler's context carries no deadline")
}
}
+59
View File
@@ -0,0 +1,59 @@
package middleware
import (
"net/http"
"runtime/debug"
)
// panicBody is the entire response a recovered panic produces. No
// template, no detail: the client learns that the request failed, and
// everything about why goes to the log and to Sentry, where it is not
// attacker-readable.
const panicBody = "internal server error"
// Recoverer turns a panicking handler into a 500 rather than a dropped
// connection.
//
// net/http already recovers panics, but what it does is close the
// connection without a response, so the client sees a transport error
// and no status. Answering 500 is the difference between "the service
// is broken" and "the network is broken" for everyone downstream.
//
// A panic after the response has started cannot be turned into a 500 —
// the status is already on the wire — so in that case the connection is
// deliberately dropped by re-panicking to net/http, which is the only
// honest signal left that the body is truncated. A truncated 200 that
// looks complete is worse than a broken connection.
func (m *Middleware) Recoverer() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := newResponseRecorder(w)
defer func() {
v := recover()
if v == nil {
return
}
// http.ErrAbortHandler is net/http's documented way for
// a handler to abandon a response on purpose. It is not
// a bug, so it is not reported; it is re-raised for
// net/http to handle as it always does.
//nolint:errorlint,err113 // a sentinel value, compared as net/http documents.
if v == http.ErrAbortHandler {
panic(v)
}
m.params.Sentry.CapturePanic(v, debug.Stack())
if rec.Written() {
panic(v)
}
http.Error(rec, panicBody, http.StatusInternalServerError)
}()
next.ServeHTTP(rec, r)
})
}
}
+63
View File
@@ -0,0 +1,63 @@
package middleware
import (
"net/http"
)
// responseRecorder remembers the status code and byte count for the
// logger and the metrics middleware. net/http gives no way to read
// them back off an http.ResponseWriter, so the only way to know what
// was answered is to be the thing that answered it.
type responseRecorder struct {
http.ResponseWriter
status int
written int64
wrote bool
}
func newResponseRecorder(w http.ResponseWriter) *responseRecorder {
// A handler that writes a body without calling WriteHeader has
// sent 200; recording that up front means Status() is right for
// the common case without waiting for a call that never comes.
return &responseRecorder{ResponseWriter: w, status: http.StatusOK}
}
func (r *responseRecorder) WriteHeader(status int) {
if r.wrote {
return
}
r.status = status
r.wrote = true
r.ResponseWriter.WriteHeader(status)
}
func (r *responseRecorder) Write(b []byte) (int, error) {
r.wrote = true
n, err := r.ResponseWriter.Write(b)
r.written += int64(n)
//nolint:wrapcheck // pass-through writer: wrapping would obscure the underlying error.
return n, err
}
// Status returns the status code that was sent.
func (r *responseRecorder) Status() int {
return r.status
}
// Written reports whether anything has been sent yet. The panic
// recoverer needs this: it can only substitute a 500 for a response
// that has not started.
func (r *responseRecorder) Written() bool {
return r.wrote
}
// Unwrap lets http.ResponseController reach the underlying writer, so
// wrapping does not cost the handler flushing or deadline control.
func (r *responseRecorder) Unwrap() http.ResponseWriter {
return r.ResponseWriter
}
+67
View File
@@ -0,0 +1,67 @@
package middleware
import "net/http"
// Security response headers.
const (
// hstsValue is served even where TLS terminates at a reverse
// proxy, so the browser enforces HTTPS end to end. Off when
// config.HSTS is false (development), because pinning a
// developer's browser to HTTPS on localhost is a self-inflicted
// outage that outlives the process.
hstsValue = "max-age=31536000; includeSubDomains"
// cspValue is the baseline. Every template ships with external CSS
// and no inline script, style or event handler, so nothing needs
// 'unsafe-inline' and nothing should be given it: the moment a
// project seeded from this template adds 'unsafe-inline', the
// policy stops being a defence against injected script and becomes
// decoration.
cspValue = "default-src 'self'; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"frame-ancestors 'none'; " +
"object-src 'none'"
// permissionsPolicyValue denies the browser features this
// application does not use.
permissionsPolicyValue = "accelerometer=(), autoplay=(), camera=(), " +
"display-capture=(), encrypted-media=(), geolocation=(), " +
"gyroscope=(), magnetometer=(), microphone=(), midi=(), " +
"payment=(), picture-in-picture=(), " +
"publickey-credentials-get=(), screen-wake-lock=(), usb=(), " +
"xr-spatial-tracking=()"
referrerPolicyValue = "strict-origin-when-cross-origin"
frameOptionsValue = "DENY"
contentTypeOptsVal = "nosniff"
)
// SecurityHeaders sets the response security headers before the handler
// runs, so they are on every response the router produces — 404s,
// handler error bodies, static assets, and the bare 500 the panic
// recoverer writes.
//
// X-Frame-Options duplicates the CSP frame-ancestors directive on
// purpose, for browsers that do not implement the latter.
func (m *Middleware) SecurityHeaders() func(http.Handler) http.Handler {
hsts := m.cfg.HSTS
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
if hsts {
h.Set("Strict-Transport-Security", hstsValue)
}
h.Set("Content-Security-Policy", cspValue)
h.Set("X-Frame-Options", frameOptionsValue)
h.Set("X-Content-Type-Options", contentTypeOptsVal)
h.Set("Referrer-Policy", referrerPolicyValue)
h.Set("Permissions-Policy", permissionsPolicyValue)
next.ServeHTTP(w, r)
})
}
}