summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/routes.go
blob: da25097bfa17f7548ff0da818361c12f3372c732 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
// 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)
}

// 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 {
		// matchers
		matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw")
		if err != nil {
			return fmt.Errorf("route %d: loading matcher modules: %v", i, err)
		}
		err = routes[i].MatcherSets.FromInterface(matchersIface)
		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 {
		handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw")
		if err != nil {
			return fmt.Errorf("route %d: loading handler modules: %v", i, err)
		}
		for _, handler := range handlersIface.([]any) {
			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(ctx, midhandler, metrics))
		}
	}
	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(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
			if !route.MatcherSets.AnyMatch(req) {
				// allow matchers the opportunity to short circuit
				// the request and trigger the error handling chain
				err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error)
				if ok {
					// clear out the error from context, otherwise
					// it will cascade to the error routes (#4916)
					SetVar(req.Context(), MatcherErrorVarKey, nil)
					// return the matcher's error
					return err
				}

				// 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(caddy.GetModuleName(mh), mh)
	}

	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: see what the std lib gives us in terms of stack tracing too
			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 []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 ms 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 '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 {
			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
}

// 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")