caddy/modules/caddyhttp/routes.go
Francis Lavoie 09b2cbcf4d
caddyhttp: Add MatchWithError to replace SetVar hack (#6596)
* caddyhttp: Add `MatchWithError` to replace SetVar hack

* Error in IP matchers on TLS handshake not complete

* Use MatchWithError everywhere possible

* Move implementations to MatchWithError versions

* Looser interface checking to allow fallback

* CEL factories can return RequestMatcherWithError

* Clarifying comment since it's subtle that an err is returned

* Return 425 Too Early status in IP matchers

* Keep AnyMatch signature the same for now

* Apparently Deprecated can't be all-uppercase to get IDE linting

* Linter
2024-11-04 23:18:50 +00:00

465 lines
16 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 == ""
}
func (r Route) String() string {
handlersRaw := "["
for _, hr := range r.HandlersRaw {
handlersRaw += " " + string(hr)
}
handlersRaw += "]"
return fmt.Sprintf(`{Group:"%s" MatcherSetsRaw:%s HandlersRaw:%s Terminal:%t}`,
r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal)
}
// Provision sets up both the matchers and handlers in the route.
func (r *Route) Provision(ctx caddy.Context, metrics *Metrics) error {
err := r.ProvisionMatchers(ctx)
if err != nil {
return err
}
return r.ProvisionHandlers(ctx, metrics)
}
// ProvisionMatchers sets up all the matchers by loading the
// matcher modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (r *Route) ProvisionMatchers(ctx caddy.Context) error {
// matchers
matchersIface, err := ctx.LoadModule(r, "MatcherSetsRaw")
if err != nil {
return fmt.Errorf("loading matcher modules: %v", err)
}
err = r.MatcherSets.FromInterface(matchersIface)
if err != nil {
return err
}
return nil
}
// ProvisionHandlers sets up all the handlers by loading the
// handler modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
handlersIface, err := ctx.LoadModule(r, "HandlersRaw")
if err != nil {
return fmt.Errorf("loading handler modules: %v", err)
}
for _, handler := range handlersIface.([]any) {
r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
}
// Make ProvisionHandlers idempotent by clearing the middleware field
r.middleware = []Middleware{}
// pre-compile the middleware handler chain
for _, midhandler := range r.Handlers {
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))
}
return nil
}
// Compile prepares a middleware chain from the route list.
// This should only be done once during the request, just
// before the middleware chain is executed.
func (r Route) Compile(next Handler) Handler {
return wrapRoute(r)(next)
}
// RouteList is a list of server routes that can
// create a middleware chain.
type RouteList []Route
// Provision sets up both the matchers and handlers in the routes.
func (routes RouteList) Provision(ctx caddy.Context) error {
err := routes.ProvisionMatchers(ctx)
if err != nil {
return err
}
return routes.ProvisionHandlers(ctx, nil)
}
// ProvisionMatchers sets up all the matchers by loading the
// matcher modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error {
for i := range routes {
err := routes[i].ProvisionMatchers(ctx)
if err != nil {
return fmt.Errorf("route %d: %v", i, err)
}
}
return nil
}
// ProvisionHandlers sets up all the handlers by loading the
// handler modules. Only call this method directly if you need
// to set up matchers and handlers separately without having
// to provision a second time; otherwise use Provision instead.
func (routes RouteList) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
for i := range routes {
err := routes[i].ProvisionHandlers(ctx, metrics)
if err != nil {
return fmt.Errorf("route %d: %v", i, err)
}
}
return nil
}
// Compile prepares a middleware chain from the route list.
// This should only be done either once during provisioning
// for top-level routes, or on each request just before the
// middleware chain is executed for subroutes.
func (routes RouteList) Compile(next Handler) Handler {
mid := make([]Middleware, 0, len(routes))
for _, route := range routes {
mid = append(mid, wrapRoute(route))
}
stack := next
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 {
// TODO: Update this comment, it seems we've moved the copy into the handler?
// 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
matches, err := route.MatcherSets.AnyMatchWithError(req)
if err != nil {
// allow matchers the opportunity to short circuit
// the request and trigger the error handling chain
return err
}
if !matches {
// call the next handler, and skip this one,
// since the matcher didn't match
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 {
if _, ok := req.Context().Value(ErrorCtxKey).(error); ok {
nextCopy = errorEmptyHandler
} else {
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(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware {
handlerToUse := mh
if metrics != nil {
// wrap the middleware with metrics instrumentation
handlerToUse = newMetricsInstrumentedHandler(ctx, caddy.GetModuleName(mh), mh, metrics)
}
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 {
// EXPERIMENTAL: Trace each module that gets invoked
if server, ok := r.Context().Value(ServerCtxKey).(*Server); ok && server != nil {
server.logTrace(handlerToUse)
}
return handlerToUse.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 []any
// 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 me, ok := m.(RequestMatcherWithError); ok {
match, _ := me.MatchWithError(r)
if !match {
return false
}
continue
}
if me, ok := m.(RequestMatcher); ok {
if !me.Match(r) {
return false
}
continue
}
return false
}
return true
}
// MatchWithError returns true if r matches m.
func (mset MatcherSet) MatchWithError(r *http.Request) (bool, error) {
for _, m := range mset {
if me, ok := m.(RequestMatcherWithError); ok {
match, err := me.MatchWithError(r)
if err != nil || !match {
return match, err
}
continue
}
if me, ok := m.(RequestMatcher); ok {
if !me.Match(r) {
// for backwards compatibility
err, ok := GetVar(r.Context(), MatcherErrorVarKey).(error)
if ok {
// clear out the error from context since we've consumed it
SetVar(r.Context(), MatcherErrorVarKey, nil)
return false, err
}
return false, nil
}
continue
}
return false, fmt.Errorf("matcher is not a RequestMatcher or RequestMatcherWithError: %#v", m)
}
return true, nil
}
// 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 ms or if there are no matchers,
// in which case the request always matches.
//
// Deprecated: Use AnyMatchWithError instead.
func (ms MatcherSets) AnyMatch(req *http.Request) bool {
for _, m := range ms {
match, err := m.MatchWithError(req)
if err != nil {
SetVar(req.Context(), MatcherErrorVarKey, err)
return false
}
if match {
return match
}
}
return len(ms) == 0
}
// AnyMatchWithError returns true if req matches any of the
// matcher sets in ms or if there are no matchers, in which
// case the request always matches. If any matcher returns
// an error, we cut short and return the error.
func (ms MatcherSets) AnyMatchWithError(req *http.Request) (bool, error) {
for _, m := range ms {
match, err := m.MatchWithError(req)
if err != nil || match {
return match, err
}
}
return len(ms) == 0, nil
}
// FromInterface fills ms from an 'any' value obtained from LoadModule.
func (ms *MatcherSets) FromInterface(matcherSets any) error {
for _, matcherSetIfaces := range matcherSets.([]map[string]any) {
var matcherSet MatcherSet
for _, matcher := range matcherSetIfaces {
if m, ok := matcher.(RequestMatcherWithError); ok {
matcherSet = append(matcherSet, m)
continue
}
if m, ok := matcher.(RequestMatcher); ok {
matcherSet = append(matcherSet, m)
continue
}
return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
}
*ms = append(*ms, matcherSet)
}
return nil
}
// TODO: Is this used?
func (ms MatcherSets) String() string {
result := "["
for _, matcherSet := range ms {
for _, matcher := range matcherSet {
result += fmt.Sprintf(" %#v", matcher)
}
}
return result + " ]"
}
var routeGroupCtxKey = caddy.CtxKey("route_group")