mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-26 02:09:47 +08:00
a5ebec0041
Previously, all matchers in a route would be evaluated before any handlers were executed, and a composite route of the matching routes would be created. This made rewrites especially tricky, since the only way to defer later matchers' evaluation was to wrap them in a subroute, or to invoke a "rehandle" which often caused bugs. Instead, this new sequential design evaluates each route's matchers then its handlers in lock-step; matcher-handlers-matcher-handlers... If the first matching route consists of a rewrite, then the second route will be evaluated against the rewritten request, rather than the original one, and so on. This should do away with any need for rehandling. I've also taken this opportunity to avoid adding new values to the request context in the handler chain, as this creates a copy of the Request struct, which may possibly lead to bugs like it has in the past (see PR #1542, PR #1481, and maybe issue #2463). We now add all the expected context values in the top-level handler at the server, then any new values can be added to the variable table via the VarsCtxKey context key, or just the GetVar/SetVar functions. In particular, we are using this facility to convey dial information in the reverse proxy. Had to be careful in one place as the middleware compilation logic has changed, and moved a bit. We no longer compile a middleware chain per- request; instead, we can compile it at provision-time, and defer only the evaluation of matchers to request-time, which should slightly improve performance. Doing this, however, we take advantage of multiple function closures, and we also changed the use of HandlerFunc (function pointer) to Handler (interface)... this led to a situation where, if we aren't careful, allows one request routed a certain way to permanently change the "next" handler for all/most other requests! We avoid this by making a copy of the interface value (which is a lightweight pointer copy) and using exclusively that within our wrapped handlers. This way, the original stack frame is preserved in a "read-only" fashion. The comments in the code describe this phenomenon. This may very well be a breaking change for some configurations, however I do not expect it to impact many people. I will make it clear in the release notes that this change has occurred.
296 lines
11 KiB
Go
296 lines
11 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddyhttp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
)
|
|
|
|
// Route consists of a set of rules for matching HTTP requests,
|
|
// a list of handlers to execute, and optional flow control
|
|
// parameters which customize the handling of HTTP requests
|
|
// in a highly flexible and performant manner.
|
|
type Route struct {
|
|
// Group is an optional name for a group to which this
|
|
// 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
|
|
// within a set are AND'ed together.
|
|
MatcherSetsRaw RawMatcherSets `json:"match,omitempty" caddy:"namespace=http.matchers"`
|
|
|
|
// The list of handlers for this route. Upon matching a request, they are chained
|
|
// together in a middleware fashion: requests flow from the first handler to the last
|
|
// (top of the list to the bottom), with the possibility that any handler could stop
|
|
// the chain and/or return an error. Responses flow back through the chain (bottom of
|
|
// the list to the top) as they are written out to the client.
|
|
//
|
|
// Not all handlers call the next handler in the chain. For example, the reverse_proxy
|
|
// handler always sends a request upstream or returns an error. Thus, configuring
|
|
// handlers after reverse_proxy in the same route is illogical, since they would never
|
|
// be executed. You will want to put handlers which originate the response at the very
|
|
// end of your route(s). The documentation for a module should state whether it invokes
|
|
// the next handler, but sometimes it is common sense.
|
|
//
|
|
// Some handlers manipulate the response. Remember that requests flow down the list, and
|
|
// responses flow up the list.
|
|
//
|
|
// For example, if you wanted to use both `templates` and `encode` handlers, you would
|
|
// need to put `templates` after `encode` in your route, because responses flow up.
|
|
// Thus, `templates` will be able to parse and execute the plain-text response as a
|
|
// template, and then return it up to the `encode` handler which will then compress it
|
|
// into a binary format.
|
|
//
|
|
// If `templates` came before `encode`, then `encode` would write a compressed,
|
|
// binary-encoded response to `templates` which would not be able to parse the response
|
|
// properly.
|
|
//
|
|
// The correct order, then, is this:
|
|
//
|
|
// [
|
|
// {"handler": "encode"},
|
|
// {"handler": "templates"},
|
|
// {"handler": "file_server"}
|
|
// ]
|
|
//
|
|
// The request flows ⬇️ DOWN (`encode` -> `templates` -> `file_server`).
|
|
//
|
|
// 1. First, `encode` will choose how to `encode` the response and wrap the response.
|
|
// 2. Then, `templates` will wrap the response with a buffer.
|
|
// 3. Finally, `file_server` will originate the content from a file.
|
|
//
|
|
// The response flows ⬆️ UP (`file_server` -> `templates` -> `encode`):
|
|
//
|
|
// 1. First, `file_server` will write the file to the response.
|
|
// 2. That write will be buffered and then executed by `templates`.
|
|
// 3. Lastly, the write from `templates` will flow into `encode` which will compress the stream.
|
|
//
|
|
// 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.
|
|
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.
|
|
func (r Route) Empty() bool {
|
|
return len(r.MatcherSetsRaw) == 0 &&
|
|
len(r.MatcherSets) == 0 &&
|
|
len(r.HandlersRaw) == 0 &&
|
|
len(r.Handlers) == 0 &&
|
|
!r.Terminal &&
|
|
r.Group == ""
|
|
}
|
|
|
|
// RouteList is a list of server routes that can
|
|
// create a middleware chain.
|
|
type RouteList []Route
|
|
|
|
// Provision sets up all the routes by loading the modules.
|
|
func (routes RouteList) Provision(ctx caddy.Context) error {
|
|
for i := range routes {
|
|
// matchers
|
|
matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw")
|
|
if err != nil {
|
|
return fmt.Errorf("loading matchers in route %d: %v", i, err)
|
|
}
|
|
err = routes[i].MatcherSets.FromInterface(matchersIface)
|
|
if err != nil {
|
|
return fmt.Errorf("route %d: %v", i, err)
|
|
}
|
|
|
|
// handlers
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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 != "" {
|
|
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{}{}
|
|
}
|
|
|
|
// make terminal routes terminate
|
|
if route.Terminal {
|
|
nextCopy = emptyHandler
|
|
}
|
|
|
|
// compile this route's handler stack
|
|
for i := len(route.middleware) - 1; i >= 0; i-- {
|
|
nextCopy = route.middleware[i](nextCopy)
|
|
}
|
|
|
|
return nextCopy.ServeHTTP(rw, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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 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, nextCopy)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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 or if there are no matchers.
|
|
func (mset MatcherSet) Match(r *http.Request) bool {
|
|
for _, m := range mset {
|
|
if !m.Match(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// RawMatcherSets is a group of matcher sets
|
|
// in their raw, JSON form.
|
|
type RawMatcherSets []caddy.ModuleMap
|
|
|
|
// MatcherSets is a group of matcher sets capable
|
|
// of checking whether a request matches any of
|
|
// the sets.
|
|
type MatcherSets []MatcherSet
|
|
|
|
// AnyMatch returns true if req matches any of the
|
|
// matcher sets in mss or if there are no matchers,
|
|
// in which case the request always matches.
|
|
func (ms MatcherSets) AnyMatch(req *http.Request) bool {
|
|
for _, m := range ms {
|
|
if m.Match(req) {
|
|
return true
|
|
}
|
|
}
|
|
return len(ms) == 0
|
|
}
|
|
|
|
// FromInterface fills ms from an interface{} value obtained from LoadModule.
|
|
func (ms *MatcherSets) FromInterface(matcherSets interface{}) error {
|
|
for _, matcherSetIfaces := range matcherSets.([]map[string]interface{}) {
|
|
var matcherSet MatcherSet
|
|
for _, matcher := range matcherSetIfaces {
|
|
reqMatcher, ok := matcher.(RequestMatcher)
|
|
if !ok {
|
|
return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher)
|
|
}
|
|
matcherSet = append(matcherSet, reqMatcher)
|
|
}
|
|
*ms = append(*ms, matcherSet)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var routeGroupCtxKey = caddy.CtxKey("route_group")
|