Raw
  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}