summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/reverseproxy
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp/reverseproxy')
-rw-r--r--modules/caddyhttp/reverseproxy/caddyfile.go80
-rw-r--r--modules/caddyhttp/reverseproxy/copyresponse.go190
-rw-r--r--modules/caddyhttp/reverseproxy/reverseproxy.go87
3 files changed, 352 insertions, 5 deletions
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index f4b1636..cab0a71 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -33,6 +33,8 @@ import (
func init() {
httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile)
+ httpcaddyfile.RegisterHandlerDirective("copy_response", parseCopyResponseCaddyfile)
+ httpcaddyfile.RegisterHandlerDirective("copy_response_headers", parseCopyResponseHeadersCaddyfile)
}
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
@@ -1019,6 +1021,84 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
+func parseCopyResponseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
+ crh := new(CopyResponseHandler)
+ err := crh.UnmarshalCaddyfile(h.Dispenser)
+ if err != nil {
+ return nil, err
+ }
+ return crh, nil
+}
+
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
+//
+// copy_response [<matcher>] [<status>] {
+// status <status>
+// }
+//
+func (h *CopyResponseHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ args := d.RemainingArgs()
+ if len(args) == 1 {
+ if num, err := strconv.Atoi(args[0]); err == nil && num > 0 {
+ h.StatusCode = caddyhttp.WeakString(args[0])
+ break
+ }
+ }
+
+ for d.NextBlock(0) {
+ switch d.Val() {
+ case "status":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ h.StatusCode = caddyhttp.WeakString(d.Val())
+ default:
+ return d.Errf("unrecognized subdirective '%s'", d.Val())
+ }
+ }
+ }
+ return nil
+}
+
+func parseCopyResponseHeadersCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
+ crh := new(CopyResponseHeadersHandler)
+ err := crh.UnmarshalCaddyfile(h.Dispenser)
+ if err != nil {
+ return nil, err
+ }
+ return crh, nil
+}
+
+// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
+//
+// copy_response_headers [<matcher>] {
+// exclude <fields...>
+// }
+//
+func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ for d.Next() {
+ args := d.RemainingArgs()
+ if len(args) > 0 {
+ return d.ArgErr()
+ }
+
+ for d.NextBlock(0) {
+ switch d.Val() {
+ case "include":
+ h.Include = append(h.Include, d.RemainingArgs()...)
+
+ case "exclude":
+ h.Exclude = append(h.Exclude, d.RemainingArgs()...)
+
+ default:
+ return d.Errf("unrecognized subdirective '%s'", d.Val())
+ }
+ }
+ }
+ return nil
+}
+
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
//
// dynamic srv [<name>] {
diff --git a/modules/caddyhttp/reverseproxy/copyresponse.go b/modules/caddyhttp/reverseproxy/copyresponse.go
new file mode 100644
index 0000000..174ffa7
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/copyresponse.go
@@ -0,0 +1,190 @@
+// 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 reverseproxy
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
+)
+
+func init() {
+ caddy.RegisterModule(CopyResponseHandler{})
+ caddy.RegisterModule(CopyResponseHeadersHandler{})
+}
+
+// CopyResponseHandler is a special HTTP handler which may
+// only be used within reverse_proxy's handle_response routes,
+// to copy the proxy response. EXPERIMENTAL, subject to change.
+type CopyResponseHandler struct {
+ // To write the upstream response's body but with a different
+ // status code, set this field to the desired status code.
+ StatusCode caddyhttp.WeakString `json:"status_code,omitempty"`
+
+ ctx caddy.Context
+}
+
+// CaddyModule returns the Caddy module information.
+func (CopyResponseHandler) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "http.handlers.copy_response",
+ New: func() caddy.Module { return new(CopyResponseHandler) },
+ }
+}
+
+// Provision ensures that h is set up properly before use.
+func (h *CopyResponseHandler) Provision(ctx caddy.Context) error {
+ h.ctx = ctx
+ return nil
+}
+
+// ServeHTTP implements the Handler interface.
+func (h CopyResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, _ caddyhttp.Handler) error {
+ repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+ hrc, ok := req.Context().Value(proxyHandleResponseContextCtxKey).(*handleResponseContext)
+
+ // don't allow this to be used outside of handle_response routes
+ if !ok {
+ return caddyhttp.Error(http.StatusInternalServerError,
+ fmt.Errorf("cannot use 'copy_response' outside of reverse_proxy's handle_response routes"))
+ }
+
+ // allow a custom status code to be written; otherwise the
+ // status code from the upstream resposne is written
+ if codeStr := h.StatusCode.String(); codeStr != "" {
+ intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
+ if err != nil {
+ return caddyhttp.Error(http.StatusInternalServerError, err)
+ }
+ hrc.response.StatusCode = intVal
+ }
+
+ // make sure the reverse_proxy handler doesn't try to call
+ // finalizeResponse again after we've already done it here.
+ hrc.isFinalized = true
+
+ // write the response
+ return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger, false)
+}
+
+// CopyResponseHeadersHandler is a special HTTP handler which may
+// only be used within reverse_proxy's handle_response routes,
+// to copy headers from the proxy response. EXPERIMENTAL;
+// subject to change.
+type CopyResponseHeadersHandler struct {
+ // A list of header fields to copy from the response.
+ // Cannot be defined at the same time as Exclude.
+ Include []string `json:"include,omitempty"`
+
+ // A list of header fields to skip copying from the response.
+ // Cannot be defined at the same time as Include.
+ Exclude []string `json:"exclude,omitempty"`
+
+ includeMap map[string]struct{}
+ excludeMap map[string]struct{}
+ ctx caddy.Context
+}
+
+// CaddyModule returns the Caddy module information.
+func (CopyResponseHeadersHandler) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "http.handlers.copy_response_headers",
+ New: func() caddy.Module { return new(CopyResponseHeadersHandler) },
+ }
+}
+
+// Validate ensures the h's configuration is valid.
+func (h *CopyResponseHeadersHandler) Validate() error {
+ if len(h.Exclude) > 0 && len(h.Include) > 0 {
+ return fmt.Errorf("cannot define both 'exclude' and 'include' lists at the same time")
+ }
+
+ return nil
+}
+
+// Provision ensures that h is set up properly before use.
+func (h *CopyResponseHeadersHandler) Provision(ctx caddy.Context) error {
+ h.ctx = ctx
+
+ // Optimize the include list by converting it to a map
+ if len(h.Include) > 0 {
+ h.includeMap = map[string]struct{}{}
+ }
+ for _, field := range h.Include {
+ h.includeMap[http.CanonicalHeaderKey(field)] = struct{}{}
+ }
+
+ // Optimize the exclude list by converting it to a map
+ if len(h.Exclude) > 0 {
+ h.excludeMap = map[string]struct{}{}
+ }
+ for _, field := range h.Exclude {
+ h.excludeMap[http.CanonicalHeaderKey(field)] = struct{}{}
+ }
+
+ return nil
+}
+
+// ServeHTTP implements the Handler interface.
+func (h CopyResponseHeadersHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next caddyhttp.Handler) error {
+ hrc, ok := req.Context().Value(proxyHandleResponseContextCtxKey).(*handleResponseContext)
+
+ // don't allow this to be used outside of handle_response routes
+ if !ok {
+ return caddyhttp.Error(http.StatusInternalServerError,
+ fmt.Errorf("cannot use 'copy_response_headers' outside of reverse_proxy's handle_response routes"))
+ }
+
+ for field, values := range hrc.response.Header {
+ // Check the include list first, skip
+ // the header if it's _not_ in this list.
+ if len(h.includeMap) > 0 {
+ if _, ok := h.includeMap[field]; !ok {
+ continue
+ }
+ }
+
+ // Then, check the exclude list, skip
+ // the header if it _is_ in this list.
+ if len(h.excludeMap) > 0 {
+ if _, ok := h.excludeMap[field]; ok {
+ continue
+ }
+ }
+
+ // Copy all the values for the header.
+ for _, value := range values {
+ rw.Header().Add(field, value)
+ }
+ }
+
+ return next.ServeHTTP(rw, req)
+}
+
+// Interface guards
+var (
+ _ caddyhttp.MiddlewareHandler = (*CopyResponseHandler)(nil)
+ _ caddyfile.Unmarshaler = (*CopyResponseHandler)(nil)
+ _ caddy.Provisioner = (*CopyResponseHandler)(nil)
+
+ _ caddyhttp.MiddlewareHandler = (*CopyResponseHeadersHandler)(nil)
+ _ caddyfile.Unmarshaler = (*CopyResponseHeadersHandler)(nil)
+ _ caddy.Provisioner = (*CopyResponseHeadersHandler)(nil)
+ _ caddy.Validator = (*CopyResponseHeadersHandler)(nil)
+)
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 3355f0b..2131a91 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -790,12 +790,33 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
h.logger.Debug("handling response", zap.Int("handler", i))
+ // we make some data available via request context to child routes
+ // so that they may inherit some options and functions from the
+ // handler, and be able to copy the response.
+ hrc := &handleResponseContext{
+ handler: h,
+ response: res,
+ start: start,
+ logger: logger,
+ }
+ ctx := req.Context()
+ ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
+
// pass the request through the response handler routes
- routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req)
+ routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req.WithContext(ctx))
+
+ // if the response handler routes already finalized the response,
+ // we can return early. It should be finalized if the routes executed
+ // included a copy_response handler. If a fresh response was written
+ // by the routes instead, then we still need to finalize the response
+ // without copying the body.
+ if routeErr == nil && hrc.isFinalized {
+ return nil
+ }
- // always close the response body afterwards since it's expected
+ // always close the response body afterwards, since it's expected
// that the response handler routes will have written to the
- // response writer with a new body
+ // response writer with a new body, if it wasn't already finalized.
res.Body.Close()
bodyClosed = true
@@ -804,8 +825,25 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
// the roundtrip was successful and to not retry
return roundtripSucceeded{routeErr}
}
+
+ // we've already closed the body, so there's no use allowing
+ // another response handler to run as well
+ break
}
+ return h.finalizeResponse(rw, req, res, repl, start, logger, bodyClosed)
+}
+
+// finalizeResponse prepares and copies the response.
+func (h Handler) finalizeResponse(
+ rw http.ResponseWriter,
+ req *http.Request,
+ res *http.Response,
+ repl *caddy.Replacer,
+ start time.Time,
+ logger *zap.Logger,
+ bodyClosed bool,
+) error {
// deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
if res.StatusCode == http.StatusSwitchingProtocols {
h.handleUpgradeResponse(logger, rw, req, res)
@@ -818,6 +856,13 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
res.Header.Del(h)
}
+ // remove the content length if we're not going to be copying
+ // from the response, because otherwise there'll be a mismatch
+ // between bytes written and the advertised length
+ if bodyClosed {
+ res.Header.Del("Content-Length")
+ }
+
// apply any response header operations
if h.Headers != nil && h.Headers.Response != nil {
if h.Headers.Response.Require == nil ||
@@ -841,7 +886,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
rw.WriteHeader(res.StatusCode)
if !bodyClosed {
- err = h.copyResponse(rw, res.Body, h.flushInterval(req, res))
+ err := h.copyResponse(rw, res.Body, h.flushInterval(req, res))
res.Body.Close() // close now, instead of defer, to populate res.Trailer
if err != nil {
// we're streaming the response and we've already written headers, so
@@ -863,7 +908,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
}
// total duration spent proxying, including writing response body
- repl.Set("http.reverse_proxy.upstream.duration", duration)
+ repl.Set("http.reverse_proxy.upstream.duration", time.Since(start))
if len(res.Trailer) == announcedTrailers {
copyHeader(rw.Header(), res.Trailer)
@@ -1227,6 +1272,38 @@ var bufPool = sync.Pool{
},
}
+// handleResponseContext carries some contextual information about the
+// the current proxy handling.
+type handleResponseContext struct {
+ // handler is the active proxy handler instance, so that
+ // routes like copy_response may inherit some config
+ // options and have access to handler methods.
+ handler *Handler
+
+ // response is the actual response received from the proxy
+ // roundtrip, to potentially be copied if a copy_response
+ // handler is in the handle_response routes.
+ response *http.Response
+
+ // start is the time just before the proxy roundtrip was
+ // performed, used for logging.
+ start time.Time
+
+ // logger is the prepared logger which is used to write logs
+ // with the request, duration, and selected upstream attached.
+ logger *zap.Logger
+
+ // isFinalized is whether the response has been finalized,
+ // i.e. copied and closed, to make sure that it doesn't
+ // happen twice.
+ isFinalized bool
+}
+
+// proxyHandleResponseContextCtxKey is the context key for the active proxy handler
+// so that handle_response routes can inherit some config options
+// from the proxy handler.
+const proxyHandleResponseContextCtxKey caddy.CtxKey = "reverse_proxy_handle_response_context"
+
// Interface guards
var (
_ caddy.Provisioner = (*Handler)(nil)