@marcel91200 / caddy Mirror · read-only / commits / 538ddb85
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 14, 05:15 UTC

reverseproxy: Enable response interception (#1447, #2920)

It's a raw, low-level implementation for now, but it's very flexible. More sugar-coating can be added after error handling is more developed.

Matthew Holt committed May 27, 2020 at 10:15 UTC 538ddb85876001976fa0c76f10b912c0870fe6d7
2 files changed +87 -15
modules/caddyhttp/reverseproxy/reverseproxy.go
+57 -15
@@ -94,6 +94,20 @@ type Handler struct {
94 // be avoided if at all possible for performance reasons.
95 BufferRequests bool `json:"buffer_requests,omitempty"`
96
97 + // List of handlers and their associated matchers to evaluate
98 + // after successful roundtrips. The first handler that matches
99 + // the response from a backend will be invoked. The response
100 + // body from the backend will not be written to the client;
101 + // it is up to the handler to finish handling the response.
102 + // If passive health checks are enabled, any errors from the
103 + // handler chain will not affect the health status of the
104 + // backend.
105 + //
106 + // Two new placeholders are available in this handler chain:
107 + // - `{http.reverse_proxy.status_code}` The status code
108 + // - `{http.reverse_proxy.status_text}` The status text
109 + HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
110 +
111 Transport http.RoundTripper `json:"-"`
112 CB CircuitBreaker `json:"-"`
113
@@ -252,6 +266,14 @@ func (h *Handler) Provision(ctx caddy.Context) error {
266 go h.activeHealthChecker()
267 }
268
269 + // set up any response routes
270 + for i, rh := range h.HandleResponse {
271 + err := rh.Provision(ctx)
272 + if err != nil {
273 + return fmt.Errorf("provisioning response handler %d: %v", i, err)
274 + }
275 + }
276 +
277 return nil
278 }
279
@@ -361,13 +383,21 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
383 }
384
385 // proxy the request to that upstream
364 - proxyErr = h.reverseProxy(w, r, dialInfo)
386 + proxyErr = h.reverseProxy(w, r, dialInfo, next)
387 if proxyErr == nil || proxyErr == context.Canceled {
388 // context.Canceled happens when the downstream client
389 // cancels the request, which is not our failure
390 return nil
391 }
392
393 + // if the roundtrip was successful, don't retry the request or
394 + // ding the health status of the upstream (an error can still
395 + // occur after the roundtrip if, for example, a response handler
396 + // after the roundtrip returns an error)
397 + if succ, ok := proxyErr.(roundtripSucceeded); ok {
398 + return succ.error
399 + }
400 +
401 // remember this failure (if enabled)
402 h.countFailure(upstream)
403
@@ -456,7 +486,7 @@ func (h Handler) prepareRequest(req *http.Request) error {
486 // reverseProxy performs a round-trip to the given backend and processes the response with the client.
487 // (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
488 // Go standard library which was used as the foundation.)
459 -func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di DialInfo) error {
489 +func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di DialInfo, next caddyhttp.Handler) error {
490 di.Upstream.Host.CountRequest(1)
491 defer di.Upstream.Host.CountRequest(-1)
492
@@ -471,16 +501,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
501 logger := h.logger.With(
502 zap.String("upstream", di.Upstream.String()),
503 zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}),
474 - zap.Duration("duration", duration),
475 - )
504 + zap.Duration("duration", duration))
505 if err != nil {
506 logger.Debug("upstream roundtrip", zap.Error(err))
507 return err
508 }
509 logger.Debug("upstream roundtrip",
510 zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)),
482 - zap.Int("status", res.StatusCode),
483 - )
511 + zap.Int("status", res.StatusCode))
512
513 // update circuit breaker on current conditions
514 if di.Upstream.cb != nil {
@@ -503,6 +531,25 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
531 }
532 }
533
534 + for i, rh := range h.HandleResponse {
535 + if len(rh.Routes) == 0 {
536 + continue
537 + }
538 + if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
539 + continue
540 + }
541 + res.Body.Close()
542 + repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
543 + repl.Set("http.reverse_proxy.status_code", res.StatusCode)
544 + repl.Set("http.reverse_proxy.status_text", res.Status)
545 + h.logger.Debug("handling response", zap.Int("handler", i))
546 + if routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req); routeErr != nil {
547 + // wrap error in roundtripSucceeded so caller knows that
548 + // the roundtrip was successful and to not retry
549 + return roundtripSucceeded{routeErr}
550 + }
551 + }
552 +
553 // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
554 if res.StatusCode == http.StatusSwitchingProtocols {
555 h.handleUpgradeResponse(rw, req, res)
@@ -537,15 +584,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
584 rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
585 }
586
540 - // TODO: there should be an option to return an error if the response
541 - // matches some criteria; would solve https://github.com/caddyserver/caddy/issues/1447
542 - // by allowing the backend to determine whether this server should treat
543 - // a 400+ status code as an error -- but we might need to be careful that
544 - // we do not affect the health status of the backend... still looking into
545 - // that; if we need to avoid that, we should return a particular error type
546 - // that the caller of this function checks for and only applies health
547 - // status changes if the error is not this special type
548 -
587 rw.WriteHeader(res.StatusCode)
588
589 err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
@@ -782,6 +820,10 @@ type TLSTransport interface {
820 EnableTLS(base *TLSConfig) error
821 }
822
823 +// roundtripSucceeded is an error type that is returned if the
824 +// roundtrip succeeded, but an error occurred after-the-fact.
825 +type roundtripSucceeded struct{ error }
826 +
827 var bufPool = sync.Pool{
828 New: func() interface{} {
829 return new(bytes.Buffer)
modules/caddyhttp/subroute.go
+30
@@ -80,6 +80,36 @@ func (sr *Subroute) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handl
80 return err
81 }
82
83 +// ResponseHandler pairs a response matcher with a route list.
84 +// It is useful for executing handler routes based on the
85 +// properties of an HTTP response that has not been written
86 +// out to the client yet.
87 +//
88 +// To use this type, provision it at module load time, then
89 +// when ready to use, match the response against its matcher;
90 +// if it matches (or doesn't have a matcher), invoke the routes
91 +// by calling `rh.Routes.Compile(next).ServeHTTP(rw, req)` (or
92 +// similar).
93 +type ResponseHandler struct {
94 + // The response matcher for this handler. If empty/nil,
95 + // it always matches.
96 + Match *ResponseMatcher `json:"match,omitempty"`
97 +
98 + // The list of HTTP routes to execute.
99 + Routes RouteList `json:"routes,omitempty"`
100 +}
101 +
102 +// Provision sets up the routse in rh.
103 +func (rh *ResponseHandler) Provision(ctx caddy.Context) error {
104 + if rh.Routes != nil {
105 + err := rh.Routes.Provision(ctx)
106 + if err != nil {
107 + return err
108 + }
109 + }
110 + return nil
111 +}
112 +
113 // Interface guards
114 var (
115 _ caddy.Provisioner = (*Subroute)(nil)