caddy/modules/caddyhttp/routes.go
Matthew Holt 2b22d2e6ea Optionally enforce strict TLS SNI + HTTP Host matching, & misc. cleanup
We should look into a way to enable this by default when TLS client auth
is configured for a server
2019-06-26 16:03:29 -06:00

206 lines
6.1 KiB
Go

package caddyhttp
import (
"encoding/json"
"fmt"
"net/http"
"github.com/caddyserver/caddy"
)
// ServerRoute represents a set of matching rules,
// middlewares, and a responder for handling HTTP
// requests.
type ServerRoute struct {
Group string `json:"group,omitempty"`
MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
Apply []json.RawMessage `json:"apply,omitempty"`
Respond json.RawMessage `json:"respond,omitempty"`
Terminal bool `json:"terminal,omitempty"`
// decoded values
matcherSets []MatcherSet
middleware []MiddlewareHandler
responder Handler
}
func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool {
for _, ms := range sr.matcherSets {
if ms.Match(r) {
return true
}
}
// if no matchers, always match
return len(sr.matcherSets) == 0
}
// MatcherSet is a set of matchers which
// must all match in order for the request
// to be matched successfully.
type MatcherSet []RequestMatcher
// Match returns true if the request matches all
// matchers in mset.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
if !m.Match(r) {
return false
}
}
return true
}
// RouteList is a list of server routes that can
// create a middleware chain.
type RouteList []ServerRoute
// Provision sets up all the routes by loading the modules.
func (routes RouteList) Provision(ctx caddy.Context) error {
for i, route := range routes {
// matchers
for _, matcherSet := range route.MatcherSets {
var matchers MatcherSet
for modName, rawMsg := range matcherSet {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
matchers = append(matchers, val.(RequestMatcher))
}
routes[i].matcherSets = append(routes[i].matcherSets, matchers)
}
routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help?
// middleware
for j, rawMsg := range route.Apply {
mid, err := ctx.LoadModuleInline("middleware", "http.middleware", rawMsg)
if err != nil {
return fmt.Errorf("loading middleware module in position %d: %v", j, err)
}
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
}
routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
// responder
if route.Respond != nil {
resp, err := ctx.LoadModuleInline("responder", "http.responders", route.Respond)
if err != nil {
return fmt.Errorf("loading responder module: %v", err)
}
routes[i].responder = resp.(Handler)
}
routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
}
return nil
}
// BuildCompositeRoute creates a chain of handlers by applying all the matching
// routes. The returned ResponseWriter should be used instead of rw.
func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Request) (Handler, http.ResponseWriter) {
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{rw}}
if len(routes) == 0 {
return emptyHandler, mrw
}
var mid []Middleware
var responder Handler
groups := make(map[string]struct{})
for _, route := range routes {
// route must match at least one of the matcher sets
if !route.anyMatcherSetMatches(req) {
continue
}
// 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
}
// this matching route satisfies the group
groups[route.Group] = struct{}{}
}
// apply the rest of the route
for _, m := range route.middleware {
// we have to be sure to wrap m outside
// of our current scope so that the
// reference to this m isn't overwritten
// on the next iteration, leaving only
// the last middleware in the chain as
// the ONLY middleware in the chain!
mid = append(mid, wrapMiddleware(m))
}
if responder == nil {
responder = route.responder
}
if route.Terminal {
break
}
}
// build the middleware stack, with the responder at the end
stack := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
if responder == nil {
return nil
}
mrw.allowWrites = true
return responder.ServeHTTP(w, r)
})
for i := len(mid) - 1; i >= 0; i-- {
stack = mid[i](stack)
}
return stack, mrw
}
// wrapMiddleware wraps m such that it can be correctly
// appended to a list of middleware. This separate closure
// is necessary so that only the last middleware in a loop
// does not become the only middleware of the stack,
// repeatedly executed (i.e. it is necessary to keep a
// reference to this m outside of the scope of a loop)!
func wrapMiddleware(m MiddlewareHandler) Middleware {
return func(next HandlerFunc) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// 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...)
// TODO: see what the std lib gives us in terms of stack tracing too
return m.ServeHTTP(w, r, next)
}
}
}
type middlewareResponseWriter struct {
*ResponseWriterWrapper
allowWrites bool
}
func (mrw middlewareResponseWriter) WriteHeader(statusCode int) {
if !mrw.allowWrites {
// technically, this is not true: middleware can write headers,
// but only after the responder handler has returned; either the
// responder did nothing with the response (sad face), or the
// middleware wrapped the response and deferred the write
panic("WriteHeader: middleware cannot write response headers")
}
mrw.ResponseWriterWrapper.WriteHeader(statusCode)
}
func (mrw middlewareResponseWriter) Write(b []byte) (int, error) {
if !mrw.allowWrites {
panic("Write: middleware cannot write to the response before responder")
}
return mrw.ResponseWriterWrapper.Write(b)
}
// Interface guard
var _ HTTPInterfaces = (*middlewareResponseWriter)(nil)