summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorFrancis Lavoie <lavofr@gmail.com>2022-05-06 10:50:26 -0400
committerGitHub <noreply@github.com>2022-05-06 10:50:26 -0400
commitf6900fcf530e80c921dac8e4f09996cffce7f436 (patch)
tree6914d9a1038ff230f9047cae8d219838b6636d81 /modules
parentec86a2f7a3905c888b8c953255a98a50be70d5db (diff)
reverseproxy: Support performing pre-check requests (#4739)
Diffstat (limited to 'modules')
-rw-r--r--modules/caddyhttp/reverseproxy/caddyfile.go29
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go10
-rw-r--r--modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go269
-rw-r--r--modules/caddyhttp/reverseproxy/reverseproxy.go56
-rw-r--r--modules/caddyhttp/rewrite/rewrite.go4
-rw-r--r--modules/caddyhttp/rewrite/rewrite_test.go2
-rw-r--r--modules/caddyhttp/standard/imports.go1
7 files changed, 353 insertions, 18 deletions
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 14de230..ebea49e 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -27,6 +27,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/dustin/go-humanize"
)
@@ -85,10 +86,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// buffer_responses
// max_buffer_size <size>
//
-// # header manipulation
+// # request manipulation
// trusted_proxies [private_ranges] <ranges...>
// header_up [+|-]<field> [<value|regexp> [<replacement>]]
// header_down [+|-]<field> [<value|regexp> [<replacement>]]
+// method <method>
+// rewrite <to>
//
// # round trip
// transport <name> {
@@ -600,6 +603,30 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Err(err.Error())
}
+ case "method":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ if h.Rewrite == nil {
+ h.Rewrite = &rewrite.Rewrite{}
+ }
+ h.Rewrite.Method = d.Val()
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+
+ case "rewrite":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ if h.Rewrite == nil {
+ h.Rewrite = &rewrite.Rewrite{}
+ }
+ h.Rewrite.URI = d.Val()
+ if d.NextArg() {
+ return d.ArgErr()
+ }
+
case "transport":
if !d.NextArg() {
return d.ArgErr()
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
index 1b4cecf..96b84f2 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -196,7 +196,15 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// NOTE: we delete the tokens as we go so that the reverse_proxy
// unmarshal doesn't see these subdirectives which it cannot handle
for dispenser.Next() {
- for dispenser.NextBlock(0) && dispenser.Nesting() == 1 {
+ for dispenser.NextBlock(0) {
+ // ignore any sub-subdirectives that might
+ // have the same name somewhere within
+ // the reverse_proxy passthrough tokens
+ if dispenser.Nesting() != 1 {
+ continue
+ }
+
+ // parse the php_fastcgi subdirectives
switch dispenser.Val() {
case "root":
if !dispenser.NextArg() {
diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
new file mode 100644
index 0000000..1571f09
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
@@ -0,0 +1,269 @@
+// 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 forwardauth
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
+)
+
+func init() {
+ httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile)
+}
+
+// parseCaddyfile parses the forward_auth directive, which has the same syntax
+// as the reverse_proxy directive (in fact, the reverse_proxy's directive
+// Unmarshaler is invoked by this function) but the resulting proxy is specially
+// configured for most™️ auth gateways that support forward auth. The typical
+// config which looks something like this:
+//
+// forward_auth auth-gateway:9091 {
+// uri /authenticate?redirect=https://auth.example.com
+// copy_headers Remote-User Remote-Email
+// }
+//
+// is equivalent to a reverse_proxy directive like this:
+//
+// reverse_proxy auth-gateway:9091 {
+// method GET
+// rewrite /authenticate?redirect=https://auth.example.com
+//
+// header_up X-Forwarded-Method {method}
+// header_up X-Forwarded-Uri {uri}
+//
+// @good status 2xx
+// handle_response @good {
+// request_header {
+// Remote-User {http.reverse_proxy.header.Remote-User}
+// Remote-Email {http.reverse_proxy.header.Remote-Email}
+// }
+// }
+//
+// handle_response {
+// copy_response_headers {
+// exclude Connection Keep-Alive Te Trailers Transfer-Encoding Upgrade
+// }
+// copy_response
+// }
+// }
+//
+func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
+ if !h.Next() {
+ return nil, h.ArgErr()
+ }
+
+ // if the user specified a matcher token, use that
+ // matcher in a route that wraps both of our routes;
+ // either way, strip the matcher token and pass
+ // the remaining tokens to the unmarshaler so that
+ // we can gain the rest of the reverse_proxy syntax
+ userMatcherSet, err := h.ExtractMatcherSet()
+ if err != nil {
+ return nil, err
+ }
+
+ // make a new dispenser from the remaining tokens so that we
+ // can reset the dispenser back to this point for the
+ // reverse_proxy unmarshaler to read from it as well
+ dispenser := h.NewFromNextSegment()
+
+ // create the reverse proxy handler
+ rpHandler := &reverseproxy.Handler{
+ // set up defaults for header_up; reverse_proxy already deals with
+ // adding the other three X-Forwarded-* headers, but for this flow,
+ // we want to also send along the incoming method and URI since this
+ // request will have a rewritten URI and method.
+ Headers: &headers.Handler{
+ Request: &headers.HeaderOps{
+ Set: http.Header{
+ "X-Forwarded-Method": []string{"{http.request.method}"},
+ "X-Forwarded-Uri": []string{"{http.request.uri}"},
+ },
+ },
+ },
+
+ // we always rewrite the method to GET, which implicitly
+ // turns off sending the incoming request's body, which
+ // allows later middleware handlers to consume it
+ Rewrite: &rewrite.Rewrite{
+ Method: "GET",
+ },
+
+ HandleResponse: []caddyhttp.ResponseHandler{},
+ }
+
+ // collect the headers to copy from the auth response
+ // onto the original request, so they can get passed
+ // through to a backend app
+ headersToCopy := []string{}
+
+ // read the subdirectives for configuring the forward_auth shortcut
+ // NOTE: we delete the tokens as we go so that the reverse_proxy
+ // unmarshal doesn't see these subdirectives which it cannot handle
+ for dispenser.Next() {
+ for dispenser.NextBlock(0) {
+ // ignore any sub-subdirectives that might
+ // have the same name somewhere within
+ // the reverse_proxy passthrough tokens
+ if dispenser.Nesting() != 1 {
+ continue
+ }
+
+ // parse the forward_auth subdirectives
+ switch dispenser.Val() {
+ case "uri":
+ if !dispenser.NextArg() {
+ return nil, dispenser.ArgErr()
+ }
+ rpHandler.Rewrite.URI = dispenser.Val()
+ dispenser.Delete()
+ dispenser.Delete()
+
+ case "copy_headers":
+ args := dispenser.RemainingArgs()
+ dispenser.Delete()
+ for _, headerField := range args {
+ dispenser.Delete()
+ headersToCopy = append(headersToCopy, headerField)
+ }
+ if len(headersToCopy) == 0 {
+ return nil, dispenser.ArgErr()
+ }
+ }
+ }
+ }
+
+ // reset the dispenser after we're done so that the reverse_proxy
+ // unmarshaler can read it from the start
+ dispenser.Reset()
+
+ // the auth target URI must not be empty
+ if rpHandler.Rewrite.URI == "" {
+ return nil, dispenser.Errf("the 'uri' subdirective is required")
+ }
+
+ // set up handler for good responses; when a response
+ // has 2xx status, then we will copy some headers from
+ // the response onto the original request, and allow
+ // handling to continue down the middleware chain,
+ // by _not_ executing a terminal handler.
+ goodResponseHandler := caddyhttp.ResponseHandler{
+ Match: &caddyhttp.ResponseMatcher{
+ StatusCode: []int{2},
+ },
+ Routes: []caddyhttp.Route{},
+ }
+ if len(headersToCopy) > 0 {
+ handler := &headers.Handler{
+ Request: &headers.HeaderOps{
+ Set: http.Header{},
+ },
+ }
+
+ for _, headerField := range headersToCopy {
+ handler.Request.Set[headerField] = []string{
+ "{http.reverse_proxy.header." + headerField + "}",
+ }
+ }
+
+ goodResponseHandler.Routes = append(
+ goodResponseHandler.Routes,
+ caddyhttp.Route{
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ handler,
+ "handler",
+ "headers",
+ nil,
+ )},
+ },
+ )
+ }
+ rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
+
+ // set up handler for denial responses; when a response
+ // has any other status than 2xx, then we copy the response
+ // back to the client, and terminate handling.
+ denialResponseHandler := caddyhttp.ResponseHandler{
+ Routes: []caddyhttp.Route{
+ {
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ &reverseproxy.CopyResponseHeadersHandler{
+ Exclude: []string{
+ "Connection",
+ "Keep-Alive",
+ "Te",
+ "Trailers",
+ "Transfer-Encoding",
+ "Upgrade",
+ },
+ },
+ "handler",
+ "copy_response_headers",
+ nil,
+ )},
+ },
+ {
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ &reverseproxy.CopyResponseHandler{},
+ "handler",
+ "copy_response",
+ nil,
+ )},
+ },
+ },
+ }
+ rpHandler.HandleResponse = append(rpHandler.HandleResponse, denialResponseHandler)
+
+ // the rest of the config is specified by the user
+ // using the reverse_proxy directive syntax
+ err = rpHandler.UnmarshalCaddyfile(dispenser)
+ if err != nil {
+ return nil, err
+ }
+ err = rpHandler.FinalizeUnmarshalCaddyfile(h)
+ if err != nil {
+ return nil, err
+ }
+
+ // create the final reverse proxy route
+ rpRoute := caddyhttp.Route{
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ rpHandler,
+ "handler",
+ "reverse_proxy",
+ nil,
+ )},
+ }
+
+ // apply the user's matcher if any
+ if userMatcherSet != nil {
+ rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet}
+ }
+
+ return []httpcaddyfile.ConfigValue{
+ {
+ Class: "route",
+ Value: rpRoute,
+ },
+ }, nil
+}
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index d69b04e..07eb99c 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -35,6 +35,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"go.uber.org/zap"
"golang.org/x/net/http/httpguts"
)
@@ -136,6 +137,18 @@ type Handler struct {
// used for the requests and responses (in bytes).
MaxBufferSize int64 `json:"max_buffer_size,omitempty"`
+ // If configured, rewrites the copy of the upstream request.
+ // Allows changing the request method and URI (path and query).
+ // Since the rewrite is applied to the copy, it does not persist
+ // past the reverse proxy handler.
+ // If the method is changed to `GET` or `HEAD`, the request body
+ // will not be copied to the backend. This allows a later request
+ // handler -- either in a `handle_response` route, or after -- to
+ // read the body.
+ // By default, no rewrite is performed, and the method and URI
+ // from the incoming request is used as-is for proxying.
+ Rewrite *rewrite.Rewrite `json:"rewrite,omitempty"`
+
// List of handlers and their associated matchers to evaluate
// after successful roundtrips. The first handler that matches
// the response from a backend will be invoked. The response
@@ -258,6 +271,13 @@ func (h *Handler) Provision(ctx caddy.Context) error {
}
}
+ if h.Rewrite != nil {
+ err := h.Rewrite.Provision(ctx)
+ if err != nil {
+ return fmt.Errorf("provisioning rewrite: %v", err)
+ }
+ }
+
// set up transport
if h.Transport == nil {
t := &HTTPTransport{}
@@ -385,7 +405,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// prepare the request for proxying; this is needed only once
- clonedReq, err := h.prepareRequest(r)
+ clonedReq, err := h.prepareRequest(r, repl)
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("preparing request for upstream round-trip: %v", err))
@@ -412,7 +432,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
var proxyErr error
for {
var done bool
- done, proxyErr = h.proxyLoopIteration(clonedReq, w, proxyErr, start, repl, reqHeader, reqHost, next)
+ done, proxyErr = h.proxyLoopIteration(clonedReq, r, w, proxyErr, start, repl, reqHeader, reqHost, next)
if done {
break
}
@@ -429,7 +449,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
// that has to be passed in, we brought this into its own method so that we could run defer more easily.
// It returns true when the loop is done and should break; false otherwise. The error value returned should
// be assigned to the proxyErr value for the next iteration of the loop (or the error handled after break).
-func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
+func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
repl *caddy.Replacer, reqHeader http.Header, reqHost string, next caddyhttp.Handler) (bool, error) {
// get the updated list of upstreams
upstreams := h.Upstreams
@@ -503,7 +523,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
}
// proxy the request to that upstream
- proxyErr = h.reverseProxy(w, r, repl, dialInfo, next)
+ proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
if proxyErr == nil || proxyErr == context.Canceled {
// context.Canceled happens when the downstream client
// cancels the request, which is not our failure
@@ -536,9 +556,20 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
// properties of the cloned request and should be done just once (before
// proxying) regardless of proxy retries. This assumes that no mutations
// of the cloned request are performed by h during or after proxying.
-func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
+func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.Request, error) {
req = cloneRequest(req)
+ // if enabled, perform rewrites on the cloned request; if
+ // the method is GET or HEAD, prevent the request body
+ // from being copied to the upstream
+ if h.Rewrite != nil {
+ changed := h.Rewrite.Rewrite(req, repl)
+ if changed && (h.Rewrite.Method == "GET" || h.Rewrite.Method == "HEAD") {
+ req.ContentLength = 0
+ req.Body = nil
+ }
+ }
+
// if enabled, buffer client request; this should only be
// enabled if the upstream requires it and does not work
// with "slow clients" (gunicorn, etc.) - this obviously
@@ -547,7 +578,7 @@ func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
// attacks, so it is strongly recommended to only use this
// feature if absolutely required, if read timeouts are
// set, and if body size is limited
- if h.BufferRequests {
+ if h.BufferRequests && req.Body != nil {
req.Body = h.bufferedBody(req.Body)
}
@@ -673,10 +704,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
// we pass through the request Host as-is, but in situations
// where we proxy over HTTPS, the user may need to override
// Host themselves, so it's helpful to send the original too.
- host, _, err := net.SplitHostPort(req.Host)
- if err != nil {
- host = req.Host // OK; there probably was no port
- }
+ host := req.Host
prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Host")
if trusted && ok && prior != "" {
host = prior
@@ -691,7 +719,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
// Go standard library which was used as the foundation.)
-func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
+func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
_ = di.Upstream.Host.countRequest(1)
//nolint:errcheck
defer di.Upstream.Host.countRequest(-1)
@@ -798,17 +826,19 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
// 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.
+ // we use the original request here, so that any routes from 'next'
+ // see the original request rather than the proxy cloned request.
hrc := &handleResponseContext{
handler: h,
response: res,
start: start,
logger: logger,
}
- ctx := req.Context()
+ ctx := origReq.Context()
ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
// pass the request through the response handler routes
- routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req.WithContext(ctx))
+ routeErr := rh.Routes.Compile(next).ServeHTTP(rw, origReq.WithContext(ctx))
// if the response handler routes already finalized the response,
// we can return early. It should be finalized if the routes executed
diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go
index cd340e3..690e752 100644
--- a/modules/caddyhttp/rewrite/rewrite.go
+++ b/modules/caddyhttp/rewrite/rewrite.go
@@ -106,7 +106,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
)
- changed := rewr.rewrite(r, repl, logger)
+ changed := rewr.Rewrite(r, repl)
if changed {
logger.Debug("rewrote request",
@@ -121,7 +121,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
// rewrite performs the rewrites on r using repl, which should
// have been obtained from r, but is passed in for efficiency.
// It returns true if any changes were made to r.
-func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
+func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
oldMethod := r.Method
oldURI := r.RequestURI
diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go
index 38d96fe..3e2e13d 100644
--- a/modules/caddyhttp/rewrite/rewrite_test.go
+++ b/modules/caddyhttp/rewrite/rewrite_test.go
@@ -292,7 +292,7 @@ func TestRewrite(t *testing.T) {
rep.re = re
}
- changed := tc.rule.rewrite(tc.input, repl, nil)
+ changed := tc.rule.Rewrite(tc.input, repl)
if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
diff --git a/modules/caddyhttp/standard/imports.go b/modules/caddyhttp/standard/imports.go
index 8ce2395..435569d 100644
--- a/modules/caddyhttp/standard/imports.go
+++ b/modules/caddyhttp/standard/imports.go
@@ -15,6 +15,7 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
+ _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/forwardauth"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing"