package web import ( "bytes" "net/http" "path" "strings" "time" transporthttp "tug/internal/transport/http" ) type Access int const ( Protected Access = iota SignedIn Public ) type Route struct { Method string Path string Prefix bool Access Access Handler http.Handler csrfFallback http.Handler } type host struct { kit *Kit routes map[string]map[string]http.Handler prefixes map[string]map[string]http.Handler patterns patternTable access map[routeKey]Access fallback http.Handler } func (k *Kit) Host(routes ...Route) http.Handler { return k.newHost(routes, nil) } func (k *Kit) newHost(routes []Route, wrap func(Route, http.Handler) http.Handler) *host { 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)} for _, rt := range routes { if rt.Handler == nil || !strings.HasPrefix(rt.Path, "/") || strings.HasPrefix(rt.Path, "/static/") { panic("web: a route needs a handler and a path outside /static/") } table := h.routes if rt.Prefix { if !strings.HasSuffix(rt.Path, "/") { panic("web: a prefix route's path ends in a slash: " + rt.Path) } table = h.prefixes } if isPattern(rt.Path) { table = h.patterns.add(rt) } if table[rt.Path] == nil { table[rt.Path] = map[string]http.Handler{} } if _, dup := table[rt.Path][rt.Method]; dup { panic("web: duplicate route " + rt.Method + " " + rt.Path) } handler := rt.Handler if rt.Method != http.MethodGet && rt.Method != http.MethodHead { handler = k.protect(handler, rt.csrfFallback) } if wrap != nil { handler = wrap(rt, handler) } table[rt.Path][rt.Method] = handler h.access[routeKey{rt.Method, rt.Path, rt.Prefix}] = rt.Access } h.patterns.refuseOverlaps(h.prefixes) return h } func (h *host) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := transporthttp.Query(r); err != nil { h.kit.ErrorPage(w, r, http.StatusBadRequest) return } p := r.URL.Path if strings.HasPrefix(p, "/static/") { h.kit.serveAsset(w, r) return } if r.URL.RawPath == "" { if methods, exact := h.routes[p]; exact { if handler, ok := methods[r.Method]; ok { handler.ServeHTTP(w, r) return } } else if handler, seg, ok := h.prefixed(p, r.Method); ok { r.SetPathValue("segment", seg) handler.ServeHTTP(w, r) return } else if handler, ok := h.patterns.match(r); ok { handler.ServeHTTP(w, r) return } } h.fallback.ServeHTTP(w, r) } func (h *host) prefixed(p, method string) (http.Handler, string, bool) { i := strings.LastIndexByte(p, '/') if i < 0 || i == len(p)-1 || path.Clean(p) != p { return nil, "", false } handler, ok := h.prefixes[p[:i+1]][method] return handler, p[i+1:], ok } func (k *Kit) serveAsset(w http.ResponseWriter, r *http.Request) { k.serveAssetAt(w, r, r.URL.Path) } func (k *Kit) serveAssetAt(w http.ResponseWriter, r *http.Request, key string) { a, ok := k.assets.byPath[key] if !ok || r.URL.RawPath != "" || (r.Method != http.MethodGet && r.Method != http.MethodHead) { k.ErrorPage(w, r, http.StatusNotFound) return } hd := w.Header() hd.Set("Content-Type", a.contentType) hd.Set("Cache-Control", "public, max-age=31536000, immutable") hd.Set("ETag", a.etag) http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(a.body)) } func (k *Kit) SSOAssets() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name, ok := strings.CutPrefix(r.URL.Path, transporthttp.SSOAssetPrefix) if !ok { k.ErrorPage(w, r, http.StatusNotFound) return } k.serveAssetAt(w, r, "/static/"+name) }) }