summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/routes.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp/routes.go')
-rw-r--r--modules/caddyhttp/routes.go168
1 files changed, 98 insertions, 70 deletions
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 9b2f849..4ae9bcd 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -28,13 +28,15 @@ import (
// in a highly flexible and performant manner.
type Route struct {
// Group is an optional name for a group to which this
- // route belongs. If a route belongs to a group, only
- // the first matching route in the group will be used.
+ // route belongs. Grouping a route makes it mutually
+ // exclusive with others in its group; if a route belongs
+ // to a group, only the first matching route in that group
+ // will be executed.
Group string `json:"group,omitempty"`
// The matcher sets which will be used to qualify this
- // route for a request. Essentially the "if" statement
- // of this route. Each matcher set is OR'ed, but matchers
+ // route for a request (essentially the "if" statement
+ // of this route). Each matcher set is OR'ed, but matchers
// within a set are AND'ed together.
MatcherSetsRaw RawMatcherSets `json:"match,omitempty" caddy:"namespace=http.matchers"`
@@ -87,12 +89,14 @@ type Route struct {
// If you think of routes in this way, it will be easy and even fun to solve the puzzle of writing correct routes.
HandlersRaw []json.RawMessage `json:"handle,omitempty" caddy:"namespace=http.handlers inline_key=handler"`
- // If true, no more routes will be executed after this one, even if they matched.
+ // If true, no more routes will be executed after this one.
Terminal bool `json:"terminal,omitempty"`
// decoded values
MatcherSets MatcherSets `json:"-"`
Handlers []MiddlewareHandler `json:"-"`
+
+ middleware []Middleware
}
// Empty returns true if the route has all zero/default values.
@@ -111,9 +115,9 @@ type RouteList []Route
// Provision sets up all the routes by loading the modules.
func (routes RouteList) Provision(ctx caddy.Context) error {
- for i, route := range routes {
+ for i := range routes {
// matchers
- matchersIface, err := ctx.LoadModule(&route, "MatcherSetsRaw")
+ matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw")
if err != nil {
return fmt.Errorf("loading matchers in route %d: %v", i, err)
}
@@ -123,93 +127,115 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
}
// handlers
- handlersIface, err := ctx.LoadModule(&route, "HandlersRaw")
+ handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw")
if err != nil {
return fmt.Errorf("loading handler modules in route %d: %v", i, err)
}
for _, handler := range handlersIface.([]interface{}) {
routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler))
}
+
+ // pre-compile the middleware handler chain
+ for _, midhandler := range routes[i].Handlers {
+ routes[i].middleware = append(routes[i].middleware, wrapMiddleware(midhandler))
+ }
}
+
return nil
}
-// BuildCompositeRoute creates a chain of handlers by
-// applying all of the matching routes.
-func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler {
- if len(routes) == 0 {
- return emptyHandler
+// Compile prepares a middleware chain from the route list.
+// This should only be done once: after all the routes have
+// been provisioned, and before serving requests.
+func (routes RouteList) Compile() Handler {
+ var mid []Middleware
+ for _, route := range routes {
+ mid = append(mid, wrapRoute(route))
+ }
+ stack := emptyHandler
+ for i := len(mid) - 1; i >= 0; i-- {
+ stack = mid[i](stack)
}
+ return stack
+}
- var mid []Middleware
- groups := make(map[string]struct{})
+// wrapRoute wraps route with a middleware and handler so that it can
+// be chained in and defer evaluation of its matchers to request-time.
+// Like wrapMiddleware, it is vital that this wrapping takes place in
+// its own stack frame so as to not overwrite the reference to the
+// intended route by looping and changing the reference each time.
+func wrapRoute(route Route) Middleware {
+ return func(next Handler) Handler {
+ return HandlerFunc(func(rw http.ResponseWriter, req *http.Request) error {
+ // copy the next handler (it's an interface, so it's just
+ // a very lightweight copy of a pointer); this is important
+ // because this is a closure to the func below, which
+ // re-assigns the value as it compiles the middleware stack;
+ // if we don't make this copy, we'd affect the underlying
+ // pointer for all future request (yikes); we could
+ // alternatively solve this by moving the func below out of
+ // this closure and into a standalone package-level func,
+ // but I just thought this made more sense
+ nextCopy := next
- for _, route := range routes {
- // route must match at least one of the matcher sets
- if !route.MatcherSets.AnyMatch(req) {
- continue
- }
+ // route must match at least one of the matcher sets
+ if !route.MatcherSets.AnyMatch(req) {
+ return nextCopy.ServeHTTP(rw, req)
+ }
- // if route is part of a group, ensure only the
- // first matching route in the group is applied
- if route.Group != "" {
- _, ok := groups[route.Group]
- if ok {
- // this group has already been satisfied
- // by a matching route
- continue
+ // if route is part of a group, ensure only the
+ // first matching route in the group is applied
+ if route.Group != "" {
+ groups := req.Context().Value(routeGroupCtxKey).(map[string]struct{})
+
+ if _, ok := groups[route.Group]; ok {
+ // this group has already been
+ // satisfied by a matching route
+ return nextCopy.ServeHTTP(rw, req)
+ }
+
+ // this matching route satisfies the group
+ groups[route.Group] = struct{}{}
}
- // this matching route satisfies the group
- groups[route.Group] = struct{}{}
- }
- // apply the rest of the route
- for _, mh := range route.Handlers {
- // we have to be sure to wrap mh outside
- // of our current stack frame so that the
- // reference to this mh isn't overwritten
- // on the next iteration, leaving the last
- // middleware in the chain as the ONLY
- // middleware in the chain!
- mid = append(mid, wrapMiddleware(mh))
- }
+ // make terminal routes terminate
+ if route.Terminal {
+ nextCopy = emptyHandler
+ }
- // if this route is supposed to be last, don't
- // compile any more into the chain
- if route.Terminal {
- break
- }
- }
+ // compile this route's handler stack
+ for i := len(route.middleware) - 1; i >= 0; i-- {
+ nextCopy = route.middleware[i](nextCopy)
+ }
- // build the middleware chain, with the responder at the end
- stack := emptyHandler
- for i := len(mid) - 1; i >= 0; i-- {
- stack = mid[i](stack)
+ return nextCopy.ServeHTTP(rw, req)
+ })
}
-
- return stack
}
-// wrapMiddleware wraps m such that it can be correctly
-// appended to a list of middleware. We can't do this
-// directly in a loop because it relies on a reference
-// to mh not changing until the execution of its handler,
-// which is deferred by multiple func closures. In other
-// words, we need to pull this particular MiddlewareHandler
+// wrapMiddleware wraps mh such that it can be correctly
+// appended to a list of middleware in preparation for
+// compiling into a handler chain. We can't do this inline
+// inside a loop, because it relies on a reference to mh
+// not changing until the execution of its handler (which
+// is deferred by multiple func closures). In other words,
+// we need to pull this particular MiddlewareHandler
// pointer into its own stack frame to preserve it so it
// won't be overwritten in future loop iterations.
func wrapMiddleware(mh MiddlewareHandler) Middleware {
- return func(next HandlerFunc) HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) error {
- // TODO: We could wait to evaluate matchers here, just eval
- // the next matcher and choose the next route...
-
- // TODO: This is where request tracing could be implemented; also
- // see below to trace the responder as well
- // TODO: Trace a diff of the request, would be cool too! see what changed since the last middleware (host, headers, URI...)
+ return func(next Handler) Handler {
+ // copy the next handler (it's an interface, so it's
+ // just a very lightweight copy of a pointer); this
+ // is a safeguard against the handler changing the
+ // value, which could affect future requests (yikes)
+ nextCopy := next
+
+ return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+ // TODO: This is where request tracing could be implemented
+ // TODO: Trace a diff of the request, would be cool too... see what changed since the last middleware (host, headers, URI...)
// TODO: see what the std lib gives us in terms of stack tracing too
- return mh.ServeHTTP(w, r, next)
- }
+ return mh.ServeHTTP(w, r, nextCopy)
+ })
}
}
@@ -219,7 +245,7 @@ func wrapMiddleware(mh MiddlewareHandler) Middleware {
type MatcherSet []RequestMatcher
// Match returns true if the request matches all
-// matchers in mset.
+// matchers in mset or if there are no matchers.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
if !m.Match(r) {
@@ -265,3 +291,5 @@ func (ms *MatcherSets) FromInterface(matcherSets interface{}) error {
}
return nil
}
+
+var routeGroupCtxKey = caddy.CtxKey("route_group")