@ -28,13 +28,15 @@ import (
// in a highly flexible and performant manner.
// in a highly flexible and performant manner.
type Route struct {
type Route struct {
// Group is an optional name for a group to which this
// Group is an optional name for a group to which this
// route belongs. If a route belongs to a group, only
// route belongs. Grouping a route makes it mutually
// the first matching route in the group will be used.
// 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" `
Group string ` json:"group,omitempty" `
// The matcher sets which will be used to qualify this
// The matcher sets which will be used to qualify this
// route for a request. E ssentially the "if" statement
// route for a request (e ssentially the "if" statement
// of this route. Each matcher set is OR'ed, but matchers
// of this route) . Each matcher set is OR'ed, but matchers
// within a set are AND'ed together.
// within a set are AND'ed together.
MatcherSetsRaw RawMatcherSets ` json:"match,omitempty" caddy:"namespace=http.matchers" `
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.
// 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" `
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" `
Terminal bool ` json:"terminal,omitempty" `
// decoded values
// decoded values
MatcherSets MatcherSets ` json:"-" `
MatcherSets MatcherSets ` json:"-" `
Handlers [ ] MiddlewareHandler ` json:"-" `
Handlers [ ] MiddlewareHandler ` json:"-" `
middleware [ ] Middleware
}
}
// Empty returns true if the route has all zero/default values.
// 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.
// Provision sets up all the routes by loading the modules.
func ( routes RouteList ) Provision ( ctx caddy . Context ) error {
func ( routes RouteList ) Provision ( ctx caddy . Context ) error {
for i , route := range routes {
for i := range routes {
// matchers
// matchers
matchersIface , err := ctx . LoadModule ( & route , "MatcherSetsRaw" )
matchersIface , err := ctx . LoadModule ( & routes [ i ] , "MatcherSetsRaw" )
if err != nil {
if err != nil {
return fmt . Errorf ( "loading matchers in route %d: %v" , i , err )
return fmt . Errorf ( "loading matchers in route %d: %v" , i , err )
}
}
@ -123,93 +127,115 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
}
}
// handlers
// handlers
handlersIface , err := ctx . LoadModule ( & route , "HandlersRaw" )
handlersIface , err := ctx . LoadModule ( & routes [ i ] , "HandlersRaw" )
if err != nil {
if err != nil {
return fmt . Errorf ( "loading handler modules in route %d: %v" , i , err )
return fmt . Errorf ( "loading handler modules in route %d: %v" , i , err )
}
}
for _ , handler := range handlersIface . ( [ ] interface { } ) {
for _ , handler := range handlersIface . ( [ ] interface { } ) {
routes [ i ] . Handlers = append ( routes [ i ] . Handlers , handler . ( MiddlewareHandler ) )
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
return nil
}
}
// BuildCompositeRoute creates a chain of handlers by
// Compile prepares a middleware chain from the route list.
// applying all of the matching routes.
// This should only be done once: after all the routes have
func ( routes RouteList ) BuildCompositeRoute ( req * http . Request ) Handler {
// been provisioned, and before serving requests.
if len ( routes ) == 0 {
func ( routes RouteList ) Compile ( ) Handler {
return emptyHandler
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
// wrapRoute wraps route with a middleware and handler so that it can
groups := make ( map [ string ] struct { } )
// 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
// route must match at least one of the matcher sets
if ! route . MatcherSets . AnyMatch ( req ) {
if ! route . MatcherSets . AnyMatch ( req ) {
return nextCopy . ServeHTTP ( rw , req )
continue
}
}
// if route is part of a group, ensure only the
// if route is part of a group, ensure only the
// first matching route in the group is applied
// first matching route in the group is applied
if route . Group != "" {
if route . Group != "" {
_ , ok := groups [ route . Group ]
groups := req . Context ( ) . Value ( routeGroupCtxKey ) . ( map [ string ] struct { } )
if ok {
// this group has already been satisfied
if _ , ok := groups [ route . Group ] ; ok {
// by a matching route
// this group has already been
continue
// 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
// make terminal routes terminate
for _ , mh := range route . Handlers {
if route . Terminal {
// we have to be sure to wrap mh outside
nextCopy = emptyHandler
// 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 ) )
}
// if this route is supposed to be last, don't
// compile this route's handler stack
// compile any more into the chain
for i := len ( route . middleware ) - 1 ; i >= 0 ; i -- {
if route . Terminal {
nextCopy = route . middleware [ i ] ( nextCopy )
break
}
}
}
// build the middleware chain, with the responder at the end
return nextCopy . ServeHTTP ( rw , req )
stack := emptyHandler
} )
for i := len ( mid ) - 1 ; i >= 0 ; i -- {
stack = mid [ i ] ( stack )
}
}
return stack
}
}
// wrapMiddleware wraps m such that it can be correctly
// wrapMiddleware wraps mh such that it can be correctly
// appended to a list of middleware. We can't do this
// appended to a list of middleware in preparation for
// directly in a loop because it relies on a reference
// compiling into a handler chain. We can't do this inline
// to mh not changing until the execution of its handler,
// inside a loop, because it relies on a reference to mh
// which is deferred by multiple func closures. In other
// not changing until the execution of its handler (which
// words, we need to pull this particular MiddlewareHandler
// 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
// pointer into its own stack frame to preserve it so it
// won't be overwritten in future loop iterations.
// won't be overwritten in future loop iterations.
func wrapMiddleware ( mh MiddlewareHandler ) Middleware {
func wrapMiddleware ( mh MiddlewareHandler ) Middleware {
return func ( next HandlerFunc ) HandlerFunc {
return func ( next Handler ) Handler {
return func ( w http . ResponseWriter , r * http . Request ) error {
// copy the next handler (it's an interface, so it's
// TODO: We could wait to evaluate matchers here, just eval
// just a very lightweight copy of a pointer); this
// the next matcher and choose the next route...
// is a safeguard against the handler changing the
// value, which could affect future requests (yikes)
// TODO: This is where request tracing could be implemented; also
nextCopy := next
// 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 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
// 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
type MatcherSet [ ] RequestMatcher
// Match returns true if the request matches all
// 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 {
func ( mset MatcherSet ) Match ( r * http . Request ) bool {
for _ , m := range mset {
for _ , m := range mset {
if ! m . Match ( r ) {
if ! m . Match ( r ) {
@ -265,3 +291,5 @@ func (ms *MatcherSets) FromInterface(matcherSets interface{}) error {
}
}
return nil
return nil
}
}
var routeGroupCtxKey = caddy . CtxKey ( "route_group" )