@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 384 lines 11.4 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package intercept
16
17 import (
18 "bytes"
19 "fmt"
20 "io"
21 "net/http"
22 "strconv"
23 "strings"
24 "sync"
25
26 "go.uber.org/zap"
27 "go.uber.org/zap/zapcore"
28
29 "github.com/caddyserver/caddy/v2"
30 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
31 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
32 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
33 )
34
35 func init() {
36 caddy.RegisterModule(Intercept{})
37 httpcaddyfile.RegisterHandlerDirective("intercept", parseCaddyfile)
38 }
39
40 // Intercept is a middleware that intercepts then replaces or modifies the original response.
41 // It can, for instance, be used to implement X-Sendfile/X-Accel-Redirect-like features
42 // when using modules like FrankenPHP or Caddy Snake.
43 //
44 // EXPERIMENTAL: Subject to change or removal.
45 type Intercept struct {
46 // List of handlers and their associated matchers to evaluate
47 // after successful response generation.
48 // The first handler that matches the original response will
49 // be invoked. The original response body will not be
50 // written to the client;
51 // it is up to the handler to finish handling the response.
52 //
53 // Three new placeholders are available in this handler chain:
54 // - `{http.intercept.status_code}` The status code from the response
55 // - `{http.intercept.header.*}` The headers from the response
56 HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
57
58 // Holds the named response matchers from the Caddyfile while adapting
59 responseMatchers map[string]caddyhttp.ResponseMatcher
60
61 // Holds the handle_response Caddyfile tokens while adapting
62 handleResponseSegments []*caddyfile.Dispenser
63
64 logger *zap.Logger
65 }
66
67 // CaddyModule returns the Caddy module information.
68 //
69 // EXPERIMENTAL: Subject to change or removal.
70 func (Intercept) CaddyModule() caddy.ModuleInfo {
71 return caddy.ModuleInfo{
72 ID: "http.handlers.intercept",
73 New: func() caddy.Module { return new(Intercept) },
74 }
75 }
76
77 // Provision ensures that i is set up properly before use.
78 //
79 // EXPERIMENTAL: Subject to change or removal.
80 func (irh *Intercept) Provision(ctx caddy.Context) error {
81 // set up any response routes
82 for i, rh := range irh.HandleResponse {
83 err := rh.Provision(ctx)
84 if err != nil {
85 return fmt.Errorf("provisioning response handler %d: %w", i, err)
86 }
87 }
88
89 irh.logger = ctx.Logger()
90
91 return nil
92 }
93
94 var bufPool = sync.Pool{
95 New: func() any {
96 return new(bytes.Buffer)
97 },
98 }
99
100 // EXPERIMENTAL: Subject to change or removal.
101 type interceptedResponseHandler struct {
102 caddyhttp.ResponseRecorder
103 replacer *caddy.Replacer
104 handler caddyhttp.ResponseHandler
105 handlerIndex int
106 statusCode int
107 }
108
109 // EXPERIMENTAL: Subject to change or removal.
110 func (irh interceptedResponseHandler) Unwrap() http.ResponseWriter {
111 return irh.ResponseRecorder
112 }
113
114 // EXPERIMENTAL: Subject to change or removal.
115 func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
116 buf := bufPool.Get().(*bytes.Buffer)
117 buf.Reset()
118 defer bufPool.Put(buf)
119
120 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
121 rec := interceptedResponseHandler{replacer: repl}
122 rec.ResponseRecorder = caddyhttp.NewResponseRecorder(w, buf, func(status int, header http.Header) bool {
123 // see if any response handler is configured for this original response
124 for i, rh := range ir.HandleResponse {
125 if rh.Match != nil && !rh.Match.Match(status, header) {
126 continue
127 }
128 rec.handler = rh
129 rec.handlerIndex = i
130
131 // if configured to only change the status code,
132 // buffer the response so we can substitute the status
133 if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" {
134 sc, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, ""))
135 if err != nil {
136 rec.statusCode = http.StatusInternalServerError
137 } else {
138 rec.statusCode = sc
139 }
140
141 return true
142 }
143
144 return rec.statusCode == 0
145 }
146
147 return false
148 })
149
150 if err := next.ServeHTTP(rec, r); err != nil {
151 return err
152 }
153 if !rec.Buffered() {
154 return nil
155 }
156
157 // set up the replacer so that parts of the original response can be
158 // used for routing decisions
159 for field, value := range rec.Header() {
160 repl.Set("http.intercept.header."+field, strings.Join(value, ","))
161 }
162 repl.Set("http.intercept.status_code", rec.Status())
163
164 if c := ir.logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
165 c.Write(zap.Int("handler", rec.handlerIndex))
166 }
167
168 // replace_status only: no routes to execute, just substitute status and write body
169 if rec.handler.Routes == nil {
170 if rec.statusCode == 0 {
171 w.WriteHeader(rec.Status())
172 } else {
173 w.WriteHeader(rec.statusCode)
174 }
175
176 if buf.Len() > 0 {
177 _, err := io.Copy(w, buf)
178
179 return err
180 }
181
182 return nil
183 }
184
185 // response recorder doesn't create a new copy of the original headers, they're
186 // present in the original response writer
187 // create a new recorder to see if any response body from the new handler is present,
188 // if not, use the already buffered response body
189 recorder := caddyhttp.NewResponseRecorder(w, nil, nil)
190 if err := rec.handler.Routes.Compile(emptyHandler).ServeHTTP(recorder, r); err != nil {
191 return err
192 }
193
194 // no new response status and the status is not 0
195 if recorder.Status() == 0 && rec.Status() != 0 {
196 w.WriteHeader(rec.Status())
197 }
198
199 // no new response body and there is some in the original response
200 // TODO: what if the new response doesn't have a body by design?
201 // see: https://github.com/caddyserver/caddy/pull/6232#issue-2235224400
202 if recorder.Size() == 0 && buf.Len() > 0 {
203 _, err := io.Copy(w, buf)
204 return err
205 }
206 return nil
207 }
208
209 // this handler does nothing because everything we need is already buffered
210 var emptyHandler caddyhttp.Handler = caddyhttp.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) error {
211 return nil
212 })
213
214 // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
215 //
216 // intercept [<matcher>] {
217 // # intercept original responses
218 // @name {
219 // status <code...>
220 // header <field> [<value>]
221 // }
222 // replace_status [<matcher>] <status_code>
223 // handle_response [<matcher>] {
224 // <directives...>
225 // }
226 // }
227 //
228 // The FinalizeUnmarshalCaddyfile method should be called after this
229 // to finalize parsing of "handle_response" blocks, if possible.
230 //
231 // EXPERIMENTAL: Subject to change or removal.
232 func (i *Intercept) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
233 // collect the response matchers defined as subdirectives
234 // prefixed with "@" for use with "handle_response" blocks
235 i.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)
236
237 d.Next() // consume the directive name
238 for d.NextBlock(0) {
239 // if the subdirective has an "@" prefix then we
240 // parse it as a response matcher for use with "handle_response"
241 if strings.HasPrefix(d.Val(), matcherPrefix) {
242 err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), i.responseMatchers)
243 if err != nil {
244 return err
245 }
246 continue
247 }
248
249 switch d.Val() {
250 case "handle_response":
251 // delegate the parsing of handle_response to the caller,
252 // since we need the httpcaddyfile.Helper to parse subroutes.
253 // See h.FinalizeUnmarshalCaddyfile
254 i.handleResponseSegments = append(i.handleResponseSegments, d.NewFromNextSegment())
255
256 case "replace_status":
257 args := d.RemainingArgs()
258 if len(args) != 1 && len(args) != 2 {
259 return d.Errf("must have one or two arguments: an optional response matcher, and a status code")
260 }
261
262 responseHandler := caddyhttp.ResponseHandler{}
263
264 if len(args) == 2 {
265 if !strings.HasPrefix(args[0], matcherPrefix) {
266 return d.Errf("must use a named response matcher, starting with '@'")
267 }
268 foundMatcher, ok := i.responseMatchers[args[0]]
269 if !ok {
270 return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
271 }
272 responseHandler.Match = &foundMatcher
273 responseHandler.StatusCode = caddyhttp.WeakString(args[1])
274 } else if len(args) == 1 {
275 responseHandler.StatusCode = caddyhttp.WeakString(args[0])
276 }
277
278 // make sure there's no block, cause it doesn't make sense
279 if nesting := d.Nesting(); d.NextBlock(nesting) {
280 return d.Errf("cannot define routes for 'replace_status', use 'handle_response' instead.")
281 }
282
283 i.HandleResponse = append(
284 i.HandleResponse,
285 responseHandler,
286 )
287
288 default:
289 return d.Errf("unrecognized subdirective %s", d.Val())
290 }
291 }
292
293 return nil
294 }
295
296 // FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which
297 // requires having an httpcaddyfile.Helper to function, to parse subroutes.
298 //
299 // EXPERIMENTAL: Subject to change or removal.
300 func (i *Intercept) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error {
301 for _, d := range i.handleResponseSegments {
302 // consume the "handle_response" token
303 d.Next()
304 args := d.RemainingArgs()
305
306 if len(args) > 1 {
307 return d.Errf("too many arguments for 'handle_response': only a single response matcher name is allowed, but got: %s", args)
308 }
309
310 var matcher *caddyhttp.ResponseMatcher
311 if len(args) == 1 {
312 // the first arg should always be a matcher.
313 if !strings.HasPrefix(args[0], matcherPrefix) {
314 return d.Errf("must use a named response matcher, starting with '@'")
315 }
316
317 foundMatcher, ok := i.responseMatchers[args[0]]
318 if !ok {
319 return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
320 }
321 matcher = &foundMatcher
322 }
323
324 // parse the block as routes
325 handler, err := httpcaddyfile.ParseSegmentAsSubroute(helper.WithDispenser(d.NewFromNextSegment()))
326 if err != nil {
327 return err
328 }
329 subroute, ok := handler.(*caddyhttp.Subroute)
330 if !ok {
331 return helper.Errf("segment was not parsed as a subroute")
332 }
333 i.HandleResponse = append(
334 i.HandleResponse,
335 caddyhttp.ResponseHandler{
336 Match: matcher,
337 Routes: subroute.Routes,
338 },
339 )
340 }
341
342 // move the handle_response entries without a matcher to the end.
343 // we can't use sort.SliceStable because it will reorder the rest of the
344 // entries which may be undesirable because we don't have a good
345 // heuristic to use for sorting.
346 withoutMatchers := []caddyhttp.ResponseHandler{}
347 withMatchers := []caddyhttp.ResponseHandler{}
348 for _, hr := range i.HandleResponse {
349 if hr.Match == nil {
350 withoutMatchers = append(withoutMatchers, hr)
351 } else {
352 withMatchers = append(withMatchers, hr)
353 }
354 }
355 i.HandleResponse = append(withMatchers, withoutMatchers...)
356
357 // clean up the bits we only needed for adapting
358 i.handleResponseSegments = nil
359 i.responseMatchers = nil
360
361 return nil
362 }
363
364 const matcherPrefix = "@"
365
366 func parseCaddyfile(helper httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
367 var ir Intercept
368 if err := ir.UnmarshalCaddyfile(helper.Dispenser); err != nil {
369 return nil, err
370 }
371
372 if err := ir.FinalizeUnmarshalCaddyfile(helper); err != nil {
373 return nil, err
374 }
375
376 return ir, nil
377 }
378
379 // Interface guards
380 var (
381 _ caddy.Provisioner = (*Intercept)(nil)
382 _ caddyfile.Unmarshaler = (*Intercept)(nil)
383 _ caddyhttp.MiddlewareHandler = (*Intercept)(nil)
384 )