package surf import ( "fmt" "net/http" "net/url" "sort" "strings" ) // Overlapping constrained routes. // // Laravel matches routes in registration order and checks ->where() // constraints while matching, so `GET /shelves/items/{id}` (id numeric) and // `GET /shelves/{shelfId}/follow` live side by side: no request satisfies // both. ServeMux only looks at the patterns and refuses the pair at // registration because neither is more specific than the other. // // compile therefore groups such routes into overlap families (the transitive // closure of "ServeMux refuses these two") and registers each family once, // under a generated method-less pattern with a wildcard wherever the members // differ. The family handler walks the members in registration order and runs // the first whose literal segments and constraints match, with its own path // values and its own fully wrapped handler. Everything outside a family // registers exactly as before. // pathSeg is one segment of a simple route path: a literal or a // single-segment wildcard. type pathSeg struct { raw string // segment text as written in the route lit string // unescaped literal; empty for a wildcard param string // wildcard name; empty for a literal } // overlapFamily is one group of routes registered behind a single pattern. type overlapFamily struct { members []int // indexes into Router.routes, in registration order pattern string // generated method-less ServeMux pattern wild []string // per segment: generated wildcard name, or "" for a shared literal } const overlapProbeSegment = "surf~overlap~probe" var noopHandler = http.NotFoundHandler() // parseSimplePath splits a route path into literal and single-segment // wildcard segments. It reports false for every other shape: the root, a // trailing-slash subtree, an empty segment, a {name...} or {$} wildcard, a // host, or a malformed escape. func parseSimplePath(path string) ([]pathSeg, bool) { if !strings.HasPrefix(path, "/") || path == "/" || strings.HasSuffix(path, "/") { return nil, false } parts := strings.Split(path[1:], "/") segs := make([]pathSeg, len(parts)) for i, part := range parts { if part == "" { return nil, false } if strings.HasPrefix(part, "{") && strings.HasSuffix(part, "}") { name := part[1 : len(part)-1] if !isWildcardName(name) { return nil, false } segs[i] = pathSeg{raw: part, param: name} continue } if strings.ContainsAny(part, "{}") { return nil, false } lit, err := url.PathUnescape(part) if err != nil { return nil, false } segs[i] = pathSeg{raw: part, lit: lit} } return segs, true } func isWildcardName(name string) bool { if name == "" { return false } for i, c := range name { switch { case c == '_', c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z': case i > 0 && c >= '0' && c <= '9': default: return false } } return true } // tryHandle registers pattern on mux and reports whether ServeMux accepted it. func tryHandle(mux *http.ServeMux, pattern string, h http.Handler) (ok bool) { defer func() { if recover() != nil { ok = false } }() mux.Handle(pattern, h) return true } // muxConflict asks ServeMux itself whether two valid patterns can share a mux. func muxConflict(a, b string) bool { if !tryHandle(http.NewServeMux(), a, noopHandler) || !tryHandle(http.NewServeMux(), b, noopHandler) { return false } mux := http.NewServeMux() mux.Handle(a, noopHandler) return !tryHandle(mux, b, noopHandler) } // mayOverlap is a cheap pre-filter: two simple paths with a different segment // count or two different literals at one position can never match the same // request. Anything else goes to muxConflict. func mayOverlap(a, b string) bool { sa, okA := parseSimplePath(a) sb, okB := parseSimplePath(b) if !okA || !okB { return true } if len(sa) != len(sb) { return false } for i := range sa { if sa[i].param == "" && sb[i].param == "" && sa[i].lit != sb[i].lit { return false } } return true } func routePattern(rt route) string { return rt.method + " " + rt.path } func routeConflict(a, b route) bool { return a.method == b.method && mayOverlap(a.path, b.path) && muxConflict(routePattern(a), routePattern(b)) } type unionFind []int func newUnionFind(n int) unionFind { u := make(unionFind, n) for i := range u { u[i] = i } return u } func (u unionFind) find(i int) int { for u[i] != i { u[i] = u[u[i]] i = u[i] } return i } // union joins two sets and reports whether they were separate. func (u unionFind) union(a, b int) bool { ra, rb := u.find(a), u.find(b) if ra == rb { return false } if ra < rb { u[rb] = ra } else { u[ra] = rb } return true } func (u unionFind) groups() [][]int { byRoot := map[int][]int{} var roots []int for i := range u { root := u.find(i) if _, ok := byRoot[root]; !ok { roots = append(roots, root) } byRoot[root] = append(byRoot[root], i) } sort.Ints(roots) var out [][]int for _, root := range roots { if members := byRoot[root]; len(members) > 1 { out = append(out, members) } } return out } // overlapFamilies finds every overlap family in routes. A route whose // pattern ServeMux rejects for another reason than a conflict is left alone // so handleRoute reports it as before. func overlapFamilies(routes []route) ([]overlapFamily, error) { uf := newUnionFind(len(routes)) probe := http.NewServeMux() var outside []int // routes the probe mux refused for j, rt := range routes { pat := routePattern(rt) if !tryHandle(http.NewServeMux(), pat, noopHandler) { continue } if tryHandle(probe, pat, noopHandler) { for _, i := range outside { if routeConflict(routes[i], rt) { uf.union(i, j) } } continue } for i := 0; i < j; i++ { if routeConflict(routes[i], rt) { uf.union(i, j) } } outside = append(outside, j) } if len(uf.groups()) == 0 { return nil, nil } // A family's generated pattern may itself overlap another route or // another family's pattern; fold those in until nothing changes. for { groups := uf.groups() fams := make([]overlapFamily, len(groups)) for fi, members := range groups { fam, err := generaliseFamily(routes, members) if err != nil { return nil, err } fams[fi] = fam } changed := false for _, fam := range fams { root := uf.find(fam.members[0]) for k, rt := range routes { if uf.find(k) == root || inAnyFamily(uf, fams, k) { continue } if mayOverlap(fam.pattern, rt.path) && muxConflict(fam.pattern, routePattern(rt)) { changed = uf.union(fam.members[0], k) || changed } } } for a := 0; a < len(fams); a++ { for b := a + 1; b < len(fams); b++ { if mayOverlap(fams[a].pattern, fams[b].pattern) && muxConflict(fams[a].pattern, fams[b].pattern) { changed = uf.union(fams[a].members[0], fams[b].members[0]) || changed } } } if !changed { return fams, nil } } } func inAnyFamily(uf unionFind, fams []overlapFamily, k int) bool { root := uf.find(k) for _, fam := range fams { if uf.find(fam.members[0]) == root { return true } } return false } func describeRoute(rt route) string { return fmt.Sprintf("%s %s (plugin %q)", rt.method, rt.path, rt.pluginID) } // generaliseFamily validates the member shapes and builds the shared pattern. func generaliseFamily(routes []route, members []int) (overlapFamily, error) { parsed := make([][]pathSeg, len(members)) for mi, idx := range members { segs, ok := parseSimplePath(routes[idx].path) if !ok { other := members[0] if other == idx { other = members[1] } return overlapFamily{}, fmt.Errorf("surf: route conflict for %s and %s: overlapping routes must use literal and single-segment {name} wildcards only", describeRoute(routes[idx]), describeRoute(routes[other])) } parsed[mi] = segs } n := len(parsed[0]) for mi := 1; mi < len(parsed); mi++ { if len(parsed[mi]) != n { return overlapFamily{}, fmt.Errorf("surf: route conflict for %s and %s: overlapping routes differ in segment count", describeRoute(routes[members[mi]]), describeRoute(routes[members[0]])) } } // Two routes of one method with the same shape differ only in parameter // names: the later one could never be reached by registration order. for a := 0; a < len(members); a++ { for b := a + 1; b < len(members); b++ { if routes[members[a]].method == routes[members[b]].method && sameShape(parsed[a], parsed[b]) { return overlapFamily{}, fmt.Errorf("surf: route conflict for %s and %s: the routes have the same shape", describeRoute(routes[members[b]]), describeRoute(routes[members[a]])) } } } wild := make([]string, n) parts := make([]string, n) for i := 0; i < n; i++ { shared := true for mi := range parsed { s := parsed[mi][i] if s.param != "" || s.lit != parsed[0][i].lit { shared = false break } } if shared { parts[i] = parsed[0][i].raw continue } wild[i] = fmt.Sprintf("surfOverlap%d", i) parts[i] = "{" + wild[i] + "}" } return overlapFamily{ members: append([]int(nil), members...), pattern: "/" + strings.Join(parts, "/"), wild: wild, }, nil } func sameShape(a, b []pathSeg) bool { if len(a) != len(b) { return false } for i := range a { if (a[i].param == "") != (b[i].param == "") { return false } if a[i].param == "" && a[i].lit != b[i].lit { return false } } return true } // familyMember is one route behind a family pattern. type familyMember struct { method string pattern string segs []pathSeg constraints []Constraint handler http.Handler } func (m familyMember) pathMatches(vals []string) bool { for i, s := range m.segs { if s.param == "" && s.lit != vals[i] { return false } } return true } func (m familyMember) constraintsMatch(vals []string) bool { for _, c := range m.constraints { value := "" for i, s := range m.segs { if s.param == c.param { value = vals[i] break } } if !c.match(value) { return false } } return true } // methodMatches mirrors ServeMux: a GET route also answers HEAD. func methodMatches(routeMethod, reqMethod string) bool { return routeMethod == reqMethod || (routeMethod == http.MethodGet && reqMethod == http.MethodHead) } // familyDispatch is the handler registered under a family pattern. type familyDispatch struct { pattern string wild []string lits []string members []familyMember methods []string // every method in the router's table, for Allow mux *http.ServeMux } func (f *familyDispatch) ServeHTTP(w http.ResponseWriter, r *http.Request) { vals := make([]string, len(f.wild)) for i, name := range f.wild { if name == "" { vals[i] = f.lits[i] } else { vals[i] = r.PathValue(name) } } methodSeen := false for _, m := range f.members { if !methodMatches(m.method, r.Method) || !m.pathMatches(vals) { continue } methodSeen = true if !m.constraintsMatch(vals) { continue } for i, s := range m.segs { if s.param != "" { r.SetPathValue(s.param, vals[i]) } } r.Pattern = m.pattern m.handler.ServeHTTP(w, r) return } if methodSeen { http.NotFound(w, r) return } allow := f.allowed(r, vals) if len(allow) == 0 { http.NotFound(w, r) return } w.Header().Set("Allow", strings.Join(allow, ", ")) http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) } // allowed lists the methods ServeMux would put in Allow for this path if the // table held the members on their own patterns: any other route the mux // matches for the method, plus members whose literal segments match. func (f *familyDispatch) allowed(r *http.Request, vals []string) []string { set := map[string]bool{} for _, method := range f.methods { _, pat := f.mux.Handler(&http.Request{Method: method, URL: r.URL, Host: r.Host}) switch pat { case "": case f.pattern: for _, m := range f.members { if m.method == method && m.pathMatches(vals) { set[method] = true break } } default: set[method] = true } } if set[http.MethodGet] { set[http.MethodHead] = true } out := make([]string, 0, len(set)) for method := range set { out = append(out, method) } sort.Strings(out) return out } // registerOverlapFamilies registers each family pattern on mux, then checks // that no other route takes a member's requests away from the family. func registerOverlapFamilies(mux *http.ServeMux, routes []route, handlers []http.Handler, fams []overlapFamily) error { if len(fams) == 0 { return nil } methodSet := map[string]bool{} for _, rt := range routes { methodSet[rt.method] = true } methods := make([]string, 0, len(methodSet)) for method := range methodSet { methods = append(methods, method) } sort.Strings(methods) for _, fam := range fams { d := &familyDispatch{pattern: fam.pattern, wild: fam.wild, methods: methods, mux: mux} d.lits = make([]string, len(fam.wild)) for _, idx := range fam.members { rt := routes[idx] segs, _ := parseSimplePath(rt.path) for i, s := range segs { if fam.wild[i] == "" { d.lits[i] = s.lit } } d.members = append(d.members, familyMember{ method: rt.method, pattern: routePattern(rt), segs: segs, constraints: rt.constraints, handler: handlers[idx], }) } if !tryHandle(mux, fam.pattern, d) { first := routes[fam.members[0]] return fmt.Errorf("surf: route conflict for %s: the overlap pattern %s cannot be registered", describeRoute(first), fam.pattern) } } for _, fam := range fams { for _, idx := range fam.members { rt := routes[idx] segs, _ := parseSimplePath(rt.path) parts := make([]string, len(segs)) for i, s := range segs { if s.param != "" { parts[i] = overlapProbeSegment } else { parts[i] = s.raw } } probe := &http.Request{Method: rt.method, URL: &url.URL{Path: "/" + strings.Join(parts, "/")}} if _, pat := mux.Handler(probe); pat != fam.pattern { return fmt.Errorf("surf: route conflict for %s: route %s takes its requests before the overlapping routes are tried", describeRoute(rt), pat) } } } return nil }