1package web
2
3import (
4 "bytes"
5 "net/http"
6 "path"
7 "strings"
8 "time"
9
10 transporthttp "tug/internal/transport/http"
11)
12
13type Access int
14
15const (
16 Protected Access = iota
17 SignedIn
18 Public
19)
20
21type Route struct {
22 Method string
23 Path string
24 Prefix bool
25 Access Access
26 Handler http.Handler
27
28 csrfFallback http.Handler
29}
30
31type host struct {
32 kit *Kit
33 routes map[string]map[string]http.Handler
34 prefixes map[string]map[string]http.Handler
35 patterns patternTable
36 access map[routeKey]Access
37 fallback http.Handler
38}
39
40func (k *Kit) Host(routes ...Route) http.Handler {
41 return k.newHost(routes, nil)
42}
43
44func (k *Kit) newHost(routes []Route, wrap func(Route, http.Handler) http.Handler) *host {
45 h := &host{kit: k, routes: map[string]map[string]http.Handler{}, prefixes: map[string]map[string]http.Handler{}, patterns: newPatternTable(), access: map[routeKey]Access{}, fallback: k.statusPage(http.StatusServiceUnavailable)}
46 for _, rt := range routes {
47 if rt.Handler == nil || !strings.HasPrefix(rt.Path, "/") || strings.HasPrefix(rt.Path, "/static/") {
48 panic("web: a route needs a handler and a path outside /static/")
49 }
50 table := h.routes
51 if rt.Prefix {
52 if !strings.HasSuffix(rt.Path, "/") {
53 panic("web: a prefix route's path ends in a slash: " + rt.Path)
54 }
55 table = h.prefixes
56 }
57 if isPattern(rt.Path) {
58 table = h.patterns.add(rt)
59 }
60 if table[rt.Path] == nil {
61 table[rt.Path] = map[string]http.Handler{}
62 }
63 if _, dup := table[rt.Path][rt.Method]; dup {
64 panic("web: duplicate route " + rt.Method + " " + rt.Path)
65 }
66 handler := rt.Handler
67 if rt.Method != http.MethodGet && rt.Method != http.MethodHead {
68 handler = k.protect(handler, rt.csrfFallback)
69 }
70 if wrap != nil {
71 handler = wrap(rt, handler)
72 }
73 table[rt.Path][rt.Method] = handler
74 h.access[routeKey{rt.Method, rt.Path, rt.Prefix}] = rt.Access
75 }
76 h.patterns.refuseOverlaps(h.prefixes)
77 return h
78}
79
80func (h *host) ServeHTTP(w http.ResponseWriter, r *http.Request) {
81 if _, err := transporthttp.Query(r); err != nil {
82 h.kit.ErrorPage(w, r, http.StatusBadRequest)
83 return
84 }
85 p := r.URL.Path
86 if strings.HasPrefix(p, "/static/") {
87 h.kit.serveAsset(w, r)
88 return
89 }
90 if r.URL.RawPath == "" {
91 if methods, exact := h.routes[p]; exact {
92 if handler, ok := methods[r.Method]; ok {
93 handler.ServeHTTP(w, r)
94 return
95 }
96 } else if handler, seg, ok := h.prefixed(p, r.Method); ok {
97 r.SetPathValue("segment", seg)
98 handler.ServeHTTP(w, r)
99 return
100 } else if handler, ok := h.patterns.match(r); ok {
101 handler.ServeHTTP(w, r)
102 return
103 }
104 }
105 h.fallback.ServeHTTP(w, r)
106}
107
108func (h *host) prefixed(p, method string) (http.Handler, string, bool) {
109 i := strings.LastIndexByte(p, '/')
110 if i < 0 || i == len(p)-1 || path.Clean(p) != p {
111 return nil, "", false
112 }
113 handler, ok := h.prefixes[p[:i+1]][method]
114 return handler, p[i+1:], ok
115}
116
117func (k *Kit) serveAsset(w http.ResponseWriter, r *http.Request) {
118 k.serveAssetAt(w, r, r.URL.Path)
119}
120
121func (k *Kit) serveAssetAt(w http.ResponseWriter, r *http.Request, key string) {
122 a, ok := k.assets.byPath[key]
123 if !ok || r.URL.RawPath != "" || (r.Method != http.MethodGet && r.Method != http.MethodHead) {
124 k.ErrorPage(w, r, http.StatusNotFound)
125 return
126 }
127 hd := w.Header()
128 hd.Set("Content-Type", a.contentType)
129 hd.Set("Cache-Control", "public, max-age=31536000, immutable")
130 hd.Set("ETag", a.etag)
131 http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(a.body))
132}
133
134func (k *Kit) SSOAssets() http.Handler {
135 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
136 name, ok := strings.CutPrefix(r.URL.Path, transporthttp.SSOAssetPrefix)
137 if !ok {
138 k.ErrorPage(w, r, http.StatusNotFound)
139 return
140 }
141 k.serveAssetAt(w, r, "/static/"+name)
142 })
143}