1package http
2
3import (
4 "fmt"
5 "log/slog"
6 "net/http"
7
8 "tug/internal/platform/config"
9)
10
11const (
12 CSP = "default-src 'none'; style-src 'self'; img-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"
13 CSPWithScript = "default-src 'none'; style-src 'self'; img-src 'self'; script-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"
14)
15
16func SecurityHeaders(next http.Handler, enhancementJS bool) http.Handler {
17 csp := CSP
18 if enhancementJS {
19 csp = CSPWithScript
20 }
21 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 h := w.Header()
23 h.Set("Content-Security-Policy", csp)
24 h.Set("Referrer-Policy", "no-referrer")
25 h.Set("X-Content-Type-Options", "nosniff")
26 next.ServeHTTP(w, r)
27 })
28}
29
30func Recover(next http.Handler, page ErrorPage) http.Handler {
31 if page == nil {
32 panic(errNilPage)
33 }
34 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35 tw := &startTracker{ResponseWriter: w}
36 defer func() {
37 v := recover()
38 if v == nil {
39 return
40 }
41 if v == http.ErrAbortHandler || tw.started {
42 panic(http.ErrAbortHandler)
43 }
44 slog.Error("handler panic before the response started", "panic", fmt.Sprint(v))
45 page(w, r, http.StatusInternalServerError)
46 }()
47 next.ServeHTTP(tw, r)
48 })
49}
50
51type startTracker struct {
52 http.ResponseWriter
53 started bool
54}
55
56func (t *startTracker) WriteHeader(code int) {
57 t.started = true
58 t.ResponseWriter.WriteHeader(code)
59}
60
61func (t *startTracker) Write(b []byte) (int, error) {
62 t.started = true
63 return t.ResponseWriter.Write(b)
64}
65
66func (t *startTracker) Flush() {
67 t.started = true
68 if f, ok := t.ResponseWriter.(http.Flusher); ok {
69 f.Flush()
70 }
71}
72
73func (t *startTracker) Unwrap() http.ResponseWriter {
74 t.started = true
75 return t.ResponseWriter
76}
77
78func Routed(srv config.Server, apps []config.App, h Handlers, page ErrorPage, enhancementJS bool, opts ...RouterOption) http.Handler {
79 if page == nil {
80 panic(errNilPage)
81 }
82 return SecurityHeaders(Recover(forwardedClient(NewRouter(srv, apps, h, page, opts...)), page), enhancementJS)
83}