Raw
  1package http
  2
  3import (
  4	"net/http"
  5	"path"
  6	"strings"
  7
  8	"tug/internal/platform/config"
  9)
 10
 11type Handlers struct {
 12	IDP       http.Handler
 13	Account   http.Handler
 14	Admin     http.Handler
 15	AuthZ     http.Handler
 16	SSO       http.Handler
 17	SSOAssets http.Handler
 18}
 19
 20type hostClass int
 21
 22const (
 23	classUnknown hostClass = iota
 24	classIDP
 25	classAccount
 26	classAdmin
 27	classApp
 28)
 29
 30type router struct {
 31	hosts   map[string]hostClass
 32	h       Handlers
 33	page    ErrorPage
 34	limits  *Limits
 35	observe Observer
 36}
 37
 38func NewRouter(srv config.Server, apps []config.App, h Handlers, page ErrorPage, opts ...RouterOption) http.Handler {
 39	if page == nil {
 40		panic(errNilPage)
 41	}
 42	hosts := map[string]hostClass{
 43		srv.DomainIDP:     classIDP,
 44		srv.DomainAccount: classAccount,
 45		srv.DomainAdmin:   classAdmin,
 46	}
 47	for _, a := range apps {
 48		hosts[a.Domain] = classApp
 49	}
 50	delete(hosts, "")
 51	rt := &router{hosts: hosts, h: h, page: page, observe: Observer{}.orDiscard()}
 52	for _, o := range opts {
 53		o(rt)
 54	}
 55	return rt
 56}
 57
 58func CanonicalHost(host string) string {
 59	h := asciiLower(host)
 60	if strings.HasPrefix(h, "[") {
 61		return h
 62	}
 63	i := strings.LastIndexByte(h, ':')
 64	if i < 0 || strings.IndexByte(h[:i], ':') >= 0 || !validPort(h[i+1:]) {
 65		return h
 66	}
 67	return h[:i]
 68}
 69
 70func asciiLower(s string) string {
 71	b := []byte(s)
 72	for i, c := range b {
 73		if 'A' <= c && c <= 'Z' {
 74			b[i] = c + ('a' - 'A')
 75		}
 76	}
 77	return string(b)
 78}
 79
 80func validPort(p string) bool {
 81	if p == "" || len(p) > 5 {
 82		return false
 83	}
 84	n := 0
 85	for _, c := range []byte(p) {
 86		if c < '0' || c > '9' {
 87			return false
 88		}
 89		n = n*10 + int(c-'0')
 90	}
 91	return n >= 1 && n <= 65535
 92}
 93
 94func (rt *router) route(w http.ResponseWriter, r *http.Request, class hostClass) {
 95	p := r.URL.Path
 96	if !strings.HasPrefix(p, "/") {
 97		rt.page(w, r, http.StatusNotFound)
 98		return
 99	}
100	clean := path.Clean("/" + p)
101	if isOperationsPath(p) || isOperationsPath(clean) {
102		rt.page(w, r, http.StatusNotFound)
103		return
104	}
105	if rt.limits != nil {
106		release, ok := rt.limits.admit(w, r, class, rt.page, rt.observe)
107		if !ok {
108			return
109		}
110		defer release()
111	}
112	switch class {
113	case classIDP:
114		rt.service(w, r, rt.h.IDP, p, clean)
115	case classAccount:
116		rt.service(w, r, rt.h.Account, p, clean)
117	case classAdmin:
118		rt.service(w, r, rt.h.Admin, p, clean)
119	case classApp:
120		rt.app(w, r)
121	default:
122		rt.page(w, r, http.StatusNotFound)
123	}
124}
125
126func isOperationsPath(p string) bool {
127	return p == "/healthz" || p == "/readyz" || p == "/metrics"
128}
129
130func isForwardAuthPath(p string) bool {
131	return p == "/authz" || strings.HasPrefix(p, "/.sso/")
132}
133
134func (rt *router) service(w http.ResponseWriter, r *http.Request, h http.Handler, p, clean string) {
135	if isForwardAuthPath(p) || isForwardAuthPath(clean) {
136		rt.page(w, r, http.StatusNotFound)
137		return
138	}
139	rt.serve(w, r, h)
140}
141
142func (rt *router) app(w http.ResponseWriter, r *http.Request) {
143	if r.URL.RawPath != "" {
144		rt.page(w, r, http.StatusNotFound)
145		return
146	}
147	switch p, m := r.URL.Path, r.Method; {
148	case p == "/authz" && m == http.MethodGet:
149		rt.serve(w, r, rt.h.AuthZ)
150	case (p == "/.sso/login" || p == "/.sso/callback") && m == http.MethodGet:
151		rt.serve(w, r, rt.h.SSO)
152	case p == "/.sso/logout" && (m == http.MethodGet || m == http.MethodPost):
153		rt.serve(w, r, rt.h.SSO)
154	case isSSOAssetPath(p) && m == http.MethodGet:
155		rt.serve(w, r, rt.h.SSOAssets)
156	default:
157		rt.page(w, r, http.StatusNotFound)
158	}
159}
160
161const SSOAssetPrefix = "/.sso/static/"
162
163func isSSOAssetPath(p string) bool {
164	name, ok := strings.CutPrefix(p, SSOAssetPrefix)
165	return ok && name != "" && !strings.Contains(name, "/") && path.Clean(p) == p
166}
167
168func (rt *router) serve(w http.ResponseWriter, r *http.Request, h http.Handler) {
169	if h == nil {
170		rt.page(w, r, http.StatusServiceUnavailable)
171		return
172	}
173	h.ServeHTTP(w, r)
174}