@marcel91200 / caddy Mirror · read-only / commits / f6900fcf
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 13, 18:00 UTC

reverseproxy: Support performing pre-check requests (#4739)

Francis Lavoie committed May 6, 2022 at 10:50 UTC f6900fcf530e80c921dac8e4f09996cffce7f436
13 files changed +539 -42
caddyconfig/httpcaddyfile/builtins.go
+14 -2
@@ -580,12 +580,24 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
580 body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
581 code = "302"
582 default:
583 + // Allow placeholders for the code
584 + if strings.HasPrefix(code, "{") {
585 + break
586 + }
587 + // Try to validate as an integer otherwise
588 codeInt, err := strconv.Atoi(code)
589 if err != nil {
590 return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)
591 }
587 - if codeInt < 300 || codeInt > 399 {
588 - return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)
592 + // Sometimes, a 401 with Location header is desirable because
593 + // requests made with XHR will "eat" the 3xx redirect; so if
594 + // the intent was to redirect to an auth page, a 3xx won't
595 + // work. Responding with 401 allows JS code to read the
596 + // Location header and do a window.location redirect manually.
597 + // see https://stackoverflow.com/a/2573589/846934
598 + // see https://github.com/oauth2-proxy/oauth2-proxy/issues/1522
599 + if codeInt < 300 || (codeInt > 399 && codeInt != 401) {
600 + return nil, h.Errf("Redir code not in the 3xx range or 401: '%v'", codeInt)
601 }
602 }
603
caddyconfig/httpcaddyfile/builtins_test.go
+18 -3
@@ -149,20 +149,35 @@ func TestRedirDirectiveSyntax(t *testing.T) {
149 expectError: false,
150 },
151 {
152 + // this is now allowed so a Location header
153 + // can be written and consumed by JS
154 + // in the case of XHR requests
155 input: `:8080 {
153 - redir /old.html /new.html htlm
156 + redir * :8081 401
157 + }`,
158 + expectError: false,
159 + },
160 + {
161 + input: `:8080 {
162 + redir * :8081 402
163 }`,
164 expectError: true,
165 },
166 {
167 input: `:8080 {
159 - redir * :8081 200
168 + redir * :8081 {http.reverse_proxy.status_code}
169 + }`,
170 + expectError: false,
171 + },
172 + {
173 + input: `:8080 {
174 + redir /old.html /new.html htlm
175 }`,
176 expectError: true,
177 },
178 {
179 input: `:8080 {
165 - redir * :8081 400
180 + redir * :8081 200
181 }`,
182 expectError: true,
183 },
caddyconfig/httpcaddyfile/directives.go
+1
@@ -57,6 +57,7 @@ var directiveOrder = []string{
57
58 // middleware handlers; some wrap responses
59 "basicauth",
60 + "forward_auth",
61 "request_header",
62 "encode",
63 "push",
caddyconfig/httpcaddyfile/httptype.go
+1
@@ -130,6 +130,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
130 {regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
131 {regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
132 {regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
133 + {regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
134 }
135
136 for _, sb := range originalServerBlocks {
caddytest/integration/caddyfile_adapt/forward_auth_authelia.txt new
+137
@@ -0,0 +1,137 @@
1 +app.example.com {
2 + forward_auth authelia:9091 {
3 + uri /api/verify?rd=https://authelia.example.com
4 + copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
5 + }
6 +
7 + reverse_proxy backend:8080
8 +}
9 +----------
10 +{
11 + "apps": {
12 + "http": {
13 + "servers": {
14 + "srv0": {
15 + "listen": [
16 + ":443"
17 + ],
18 + "routes": [
19 + {
20 + "match": [
21 + {
22 + "host": [
23 + "app.example.com"
24 + ]
25 + }
26 + ],
27 + "handle": [
28 + {
29 + "handler": "subroute",
30 + "routes": [
31 + {
32 + "handle": [
33 + {
34 + "handle_response": [
35 + {
36 + "match": {
37 + "status_code": [
38 + 2
39 + ]
40 + },
41 + "routes": [
42 + {
43 + "handle": [
44 + {
45 + "handler": "headers",
46 + "request": {
47 + "set": {
48 + "Remote-Email": [
49 + "{http.reverse_proxy.header.Remote-Email}"
50 + ],
51 + "Remote-Groups": [
52 + "{http.reverse_proxy.header.Remote-Groups}"
53 + ],
54 + "Remote-Name": [
55 + "{http.reverse_proxy.header.Remote-Name}"
56 + ],
57 + "Remote-User": [
58 + "{http.reverse_proxy.header.Remote-User}"
59 + ]
60 + }
61 + }
62 + }
63 + ]
64 + }
65 + ]
66 + },
67 + {
68 + "routes": [
69 + {
70 + "handle": [
71 + {
72 + "exclude": [
73 + "Connection",
74 + "Keep-Alive",
75 + "Te",
76 + "Trailers",
77 + "Transfer-Encoding",
78 + "Upgrade"
79 + ],
80 + "handler": "copy_response_headers"
81 + }
82 + ]
83 + },
84 + {
85 + "handle": [
86 + {
87 + "handler": "copy_response"
88 + }
89 + ]
90 + }
91 + ]
92 + }
93 + ],
94 + "handler": "reverse_proxy",
95 + "headers": {
96 + "request": {
97 + "set": {
98 + "X-Forwarded-Method": [
99 + "{http.request.method}"
100 + ],
101 + "X-Forwarded-Uri": [
102 + "{http.request.uri}"
103 + ]
104 + }
105 + }
106 + },
107 + "rewrite": {
108 + "method": "GET",
109 + "uri": "/api/verify?rd=https://authelia.example.com"
110 + },
111 + "upstreams": [
112 + {
113 + "dial": "authelia:9091"
114 + }
115 + ]
116 + },
117 + {
118 + "handler": "reverse_proxy",
119 + "upstreams": [
120 + {
121 + "dial": "backend:8080"
122 + }
123 + ]
124 + }
125 + ]
126 + }
127 + ]
128 + }
129 + ],
130 + "terminal": true
131 + }
132 + ]
133 + }
134 + }
135 + }
136 + }
137 +}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt
+15 -19
@@ -1,11 +1,11 @@
1
2 https://example.com {
3 - reverse_proxy /path http://localhost:54321 {
4 - header_up Host {host}
5 - header_up X-Real-IP {remote}
6 - header_up X-Forwarded-For {remote}
7 - header_up X-Forwarded-Port {server_port}
8 - header_up X-Forwarded-Proto "http"
3 + reverse_proxy /path https://localhost:54321 {
4 + header_up Host {upstream_hostport}
5 + header_up Foo bar
6 +
7 + method GET
8 + rewrite /rewritten?uri={uri}
9
10 buffer_requests
11
@@ -58,24 +58,19 @@ https://example.com {
58 "headers": {
59 "request": {
60 "set": {
61 - "Host": [
62 - "{http.request.host}"
63 - ],
64 - "X-Forwarded-For": [
65 - "{http.request.remote}"
66 - ],
67 - "X-Forwarded-Port": [
68 - "{server_port}"
61 + "Foo": [
62 + "bar"
63 ],
70 - "X-Forwarded-Proto": [
71 - "http"
72 - ],
73 - "X-Real-Ip": [
74 - "{http.request.remote}"
64 + "Host": [
65 + "{http.reverse_proxy.upstream.hostport}"
66 ]
67 }
68 }
69 },
70 + "rewrite": {
71 + "method": "GET",
72 + "uri": "/rewritten?uri={http.request.uri}"
73 + },
74 "transport": {
75 "compression": false,
76 "dial_fallback_delay": 5000000000,
@@ -96,6 +91,7 @@ https://example.com {
91 ]
92 },
93 "response_header_timeout": 8000000000,
94 + "tls": {},
95 "versions": [
96 "h2c",
97 "2"
modules/caddyhttp/reverseproxy/caddyfile.go
+28 -1
@@ -27,6 +27,7 @@ import (
27 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
28 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
30 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
31 "github.com/dustin/go-humanize"
32 )
33
@@ -85,10 +86,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
86 // buffer_responses
87 // max_buffer_size <size>
88 //
88 -// # header manipulation
89 +// # request manipulation
90 // trusted_proxies [private_ranges] <ranges...>
91 // header_up [+|-]<field> [<value|regexp> [<replacement>]]
92 // header_down [+|-]<field> [<value|regexp> [<replacement>]]
93 +// method <method>
94 +// rewrite <to>
95 //
96 // # round trip
97 // transport <name> {
@@ -600,6 +603,30 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
603 return d.Err(err.Error())
604 }
605
606 + case "method":
607 + if !d.NextArg() {
608 + return d.ArgErr()
609 + }
610 + if h.Rewrite == nil {
611 + h.Rewrite = &rewrite.Rewrite{}
612 + }
613 + h.Rewrite.Method = d.Val()
614 + if d.NextArg() {
615 + return d.ArgErr()
616 + }
617 +
618 + case "rewrite":
619 + if !d.NextArg() {
620 + return d.ArgErr()
621 + }
622 + if h.Rewrite == nil {
623 + h.Rewrite = &rewrite.Rewrite{}
624 + }
625 + h.Rewrite.URI = d.Val()
626 + if d.NextArg() {
627 + return d.ArgErr()
628 + }
629 +
630 case "transport":
631 if !d.NextArg() {
632 return d.ArgErr()
modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+9 -1
@@ -196,7 +196,15 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
196 // NOTE: we delete the tokens as we go so that the reverse_proxy
197 // unmarshal doesn't see these subdirectives which it cannot handle
198 for dispenser.Next() {
199 - for dispenser.NextBlock(0) && dispenser.Nesting() == 1 {
199 + for dispenser.NextBlock(0) {
200 + // ignore any sub-subdirectives that might
201 + // have the same name somewhere within
202 + // the reverse_proxy passthrough tokens
203 + if dispenser.Nesting() != 1 {
204 + continue
205 + }
206 +
207 + // parse the php_fastcgi subdirectives
208 switch dispenser.Val() {
209 case "root":
210 if !dispenser.NextArg() {
modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go new
+269
@@ -0,0 +1,269 @@
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 forwardauth
16 +
17 +import (
18 + "encoding/json"
19 + "net/http"
20 +
21 + "github.com/caddyserver/caddy/v2"
22 + "github.com/caddyserver/caddy/v2/caddyconfig"
23 + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
24 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
25 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
26 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
27 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
28 +)
29 +
30 +func init() {
31 + httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile)
32 +}
33 +
34 +// parseCaddyfile parses the forward_auth directive, which has the same syntax
35 +// as the reverse_proxy directive (in fact, the reverse_proxy's directive
36 +// Unmarshaler is invoked by this function) but the resulting proxy is specially
37 +// configured for most™️ auth gateways that support forward auth. The typical
38 +// config which looks something like this:
39 +//
40 +// forward_auth auth-gateway:9091 {
41 +// uri /authenticate?redirect=https://auth.example.com
42 +// copy_headers Remote-User Remote-Email
43 +// }
44 +//
45 +// is equivalent to a reverse_proxy directive like this:
46 +//
47 +// reverse_proxy auth-gateway:9091 {
48 +// method GET
49 +// rewrite /authenticate?redirect=https://auth.example.com
50 +//
51 +// header_up X-Forwarded-Method {method}
52 +// header_up X-Forwarded-Uri {uri}
53 +//
54 +// @good status 2xx
55 +// handle_response @good {
56 +// request_header {
57 +// Remote-User {http.reverse_proxy.header.Remote-User}
58 +// Remote-Email {http.reverse_proxy.header.Remote-Email}
59 +// }
60 +// }
61 +//
62 +// handle_response {
63 +// copy_response_headers {
64 +// exclude Connection Keep-Alive Te Trailers Transfer-Encoding Upgrade
65 +// }
66 +// copy_response
67 +// }
68 +// }
69 +//
70 +func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
71 + if !h.Next() {
72 + return nil, h.ArgErr()
73 + }
74 +
75 + // if the user specified a matcher token, use that
76 + // matcher in a route that wraps both of our routes;
77 + // either way, strip the matcher token and pass
78 + // the remaining tokens to the unmarshaler so that
79 + // we can gain the rest of the reverse_proxy syntax
80 + userMatcherSet, err := h.ExtractMatcherSet()
81 + if err != nil {
82 + return nil, err
83 + }
84 +
85 + // make a new dispenser from the remaining tokens so that we
86 + // can reset the dispenser back to this point for the
87 + // reverse_proxy unmarshaler to read from it as well
88 + dispenser := h.NewFromNextSegment()
89 +
90 + // create the reverse proxy handler
91 + rpHandler := &reverseproxy.Handler{
92 + // set up defaults for header_up; reverse_proxy already deals with
93 + // adding the other three X-Forwarded-* headers, but for this flow,
94 + // we want to also send along the incoming method and URI since this
95 + // request will have a rewritten URI and method.
96 + Headers: &headers.Handler{
97 + Request: &headers.HeaderOps{
98 + Set: http.Header{
99 + "X-Forwarded-Method": []string{"{http.request.method}"},
100 + "X-Forwarded-Uri": []string{"{http.request.uri}"},
101 + },
102 + },
103 + },
104 +
105 + // we always rewrite the method to GET, which implicitly
106 + // turns off sending the incoming request's body, which
107 + // allows later middleware handlers to consume it
108 + Rewrite: &rewrite.Rewrite{
109 + Method: "GET",
110 + },
111 +
112 + HandleResponse: []caddyhttp.ResponseHandler{},
113 + }
114 +
115 + // collect the headers to copy from the auth response
116 + // onto the original request, so they can get passed
117 + // through to a backend app
118 + headersToCopy := []string{}
119 +
120 + // read the subdirectives for configuring the forward_auth shortcut
121 + // NOTE: we delete the tokens as we go so that the reverse_proxy
122 + // unmarshal doesn't see these subdirectives which it cannot handle
123 + for dispenser.Next() {
124 + for dispenser.NextBlock(0) {
125 + // ignore any sub-subdirectives that might
126 + // have the same name somewhere within
127 + // the reverse_proxy passthrough tokens
128 + if dispenser.Nesting() != 1 {
129 + continue
130 + }
131 +
132 + // parse the forward_auth subdirectives
133 + switch dispenser.Val() {
134 + case "uri":
135 + if !dispenser.NextArg() {
136 + return nil, dispenser.ArgErr()
137 + }
138 + rpHandler.Rewrite.URI = dispenser.Val()
139 + dispenser.Delete()
140 + dispenser.Delete()
141 +
142 + case "copy_headers":
143 + args := dispenser.RemainingArgs()
144 + dispenser.Delete()
145 + for _, headerField := range args {
146 + dispenser.Delete()
147 + headersToCopy = append(headersToCopy, headerField)
148 + }
149 + if len(headersToCopy) == 0 {
150 + return nil, dispenser.ArgErr()
151 + }
152 + }
153 + }
154 + }
155 +
156 + // reset the dispenser after we're done so that the reverse_proxy
157 + // unmarshaler can read it from the start
158 + dispenser.Reset()
159 +
160 + // the auth target URI must not be empty
161 + if rpHandler.Rewrite.URI == "" {
162 + return nil, dispenser.Errf("the 'uri' subdirective is required")
163 + }
164 +
165 + // set up handler for good responses; when a response
166 + // has 2xx status, then we will copy some headers from
167 + // the response onto the original request, and allow
168 + // handling to continue down the middleware chain,
169 + // by _not_ executing a terminal handler.
170 + goodResponseHandler := caddyhttp.ResponseHandler{
171 + Match: &caddyhttp.ResponseMatcher{
172 + StatusCode: []int{2},
173 + },
174 + Routes: []caddyhttp.Route{},
175 + }
176 + if len(headersToCopy) > 0 {
177 + handler := &headers.Handler{
178 + Request: &headers.HeaderOps{
179 + Set: http.Header{},
180 + },
181 + }
182 +
183 + for _, headerField := range headersToCopy {
184 + handler.Request.Set[headerField] = []string{
185 + "{http.reverse_proxy.header." + headerField + "}",
186 + }
187 + }
188 +
189 + goodResponseHandler.Routes = append(
190 + goodResponseHandler.Routes,
191 + caddyhttp.Route{
192 + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
193 + handler,
194 + "handler",
195 + "headers",
196 + nil,
197 + )},
198 + },
199 + )
200 + }
201 + rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
202 +
203 + // set up handler for denial responses; when a response
204 + // has any other status than 2xx, then we copy the response
205 + // back to the client, and terminate handling.
206 + denialResponseHandler := caddyhttp.ResponseHandler{
207 + Routes: []caddyhttp.Route{
208 + {
209 + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
210 + &reverseproxy.CopyResponseHeadersHandler{
211 + Exclude: []string{
212 + "Connection",
213 + "Keep-Alive",
214 + "Te",
215 + "Trailers",
216 + "Transfer-Encoding",
217 + "Upgrade",
218 + },
219 + },
220 + "handler",
221 + "copy_response_headers",
222 + nil,
223 + )},
224 + },
225 + {
226 + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
227 + &reverseproxy.CopyResponseHandler{},
228 + "handler",
229 + "copy_response",
230 + nil,
231 + )},
232 + },
233 + },
234 + }
235 + rpHandler.HandleResponse = append(rpHandler.HandleResponse, denialResponseHandler)
236 +
237 + // the rest of the config is specified by the user
238 + // using the reverse_proxy directive syntax
239 + err = rpHandler.UnmarshalCaddyfile(dispenser)
240 + if err != nil {
241 + return nil, err
242 + }
243 + err = rpHandler.FinalizeUnmarshalCaddyfile(h)
244 + if err != nil {
245 + return nil, err
246 + }
247 +
248 + // create the final reverse proxy route
249 + rpRoute := caddyhttp.Route{
250 + HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
251 + rpHandler,
252 + "handler",
253 + "reverse_proxy",
254 + nil,
255 + )},
256 + }
257 +
258 + // apply the user's matcher if any
259 + if userMatcherSet != nil {
260 + rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet}
261 + }
262 +
263 + return []httpcaddyfile.ConfigValue{
264 + {
265 + Class: "route",
266 + Value: rpRoute,
267 + },
268 + }, nil
269 +}
modules/caddyhttp/reverseproxy/reverseproxy.go
+43 -13
@@ -35,6 +35,7 @@ import (
35 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
36 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
37 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
38 + "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
39 "go.uber.org/zap"
40 "golang.org/x/net/http/httpguts"
41 )
@@ -136,6 +137,18 @@ type Handler struct {
137 // used for the requests and responses (in bytes).
138 MaxBufferSize int64 `json:"max_buffer_size,omitempty"`
139
140 + // If configured, rewrites the copy of the upstream request.
141 + // Allows changing the request method and URI (path and query).
142 + // Since the rewrite is applied to the copy, it does not persist
143 + // past the reverse proxy handler.
144 + // If the method is changed to `GET` or `HEAD`, the request body
145 + // will not be copied to the backend. This allows a later request
146 + // handler -- either in a `handle_response` route, or after -- to
147 + // read the body.
148 + // By default, no rewrite is performed, and the method and URI
149 + // from the incoming request is used as-is for proxying.
150 + Rewrite *rewrite.Rewrite `json:"rewrite,omitempty"`
151 +
152 // List of handlers and their associated matchers to evaluate
153 // after successful roundtrips. The first handler that matches
154 // the response from a backend will be invoked. The response
@@ -258,6 +271,13 @@ func (h *Handler) Provision(ctx caddy.Context) error {
271 }
272 }
273
274 + if h.Rewrite != nil {
275 + err := h.Rewrite.Provision(ctx)
276 + if err != nil {
277 + return fmt.Errorf("provisioning rewrite: %v", err)
278 + }
279 + }
280 +
281 // set up transport
282 if h.Transport == nil {
283 t := &HTTPTransport{}
@@ -385,7 +405,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
405 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
406
407 // prepare the request for proxying; this is needed only once
388 - clonedReq, err := h.prepareRequest(r)
408 + clonedReq, err := h.prepareRequest(r, repl)
409 if err != nil {
410 return caddyhttp.Error(http.StatusInternalServerError,
411 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
432 var proxyErr error
433 for {
434 var done bool
415 - done, proxyErr = h.proxyLoopIteration(clonedReq, w, proxyErr, start, repl, reqHeader, reqHost, next)
435 + done, proxyErr = h.proxyLoopIteration(clonedReq, r, w, proxyErr, start, repl, reqHeader, reqHost, next)
436 if done {
437 break
438 }
@@ -429,7 +449,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
449 // that has to be passed in, we brought this into its own method so that we could run defer more easily.
450 // It returns true when the loop is done and should break; false otherwise. The error value returned should
451 // be assigned to the proxyErr value for the next iteration of the loop (or the error handled after break).
432 -func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
452 +func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w http.ResponseWriter, proxyErr error, start time.Time,
453 repl *caddy.Replacer, reqHeader http.Header, reqHost string, next caddyhttp.Handler) (bool, error) {
454 // get the updated list of upstreams
455 upstreams := h.Upstreams
@@ -503,7 +523,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
523 }
524
525 // proxy the request to that upstream
506 - proxyErr = h.reverseProxy(w, r, repl, dialInfo, next)
526 + proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
527 if proxyErr == nil || proxyErr == context.Canceled {
528 // context.Canceled happens when the downstream client
529 // cancels the request, which is not our failure
@@ -536,9 +556,20 @@ func (h *Handler) proxyLoopIteration(r *http.Request, w http.ResponseWriter, pro
556 // properties of the cloned request and should be done just once (before
557 // proxying) regardless of proxy retries. This assumes that no mutations
558 // of the cloned request are performed by h during or after proxying.
539 -func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
559 +func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.Request, error) {
560 req = cloneRequest(req)
561
562 + // if enabled, perform rewrites on the cloned request; if
563 + // the method is GET or HEAD, prevent the request body
564 + // from being copied to the upstream
565 + if h.Rewrite != nil {
566 + changed := h.Rewrite.Rewrite(req, repl)
567 + if changed && (h.Rewrite.Method == "GET" || h.Rewrite.Method == "HEAD") {
568 + req.ContentLength = 0
569 + req.Body = nil
570 + }
571 + }
572 +
573 // if enabled, buffer client request; this should only be
574 // enabled if the upstream requires it and does not work
575 // with "slow clients" (gunicorn, etc.) - this obviously
@@ -547,7 +578,7 @@ func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
578 // attacks, so it is strongly recommended to only use this
579 // feature if absolutely required, if read timeouts are
580 // set, and if body size is limited
550 - if h.BufferRequests {
581 + if h.BufferRequests && req.Body != nil {
582 req.Body = h.bufferedBody(req.Body)
583 }
584
@@ -673,10 +704,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
704 // we pass through the request Host as-is, but in situations
705 // where we proxy over HTTPS, the user may need to override
706 // Host themselves, so it's helpful to send the original too.
676 - host, _, err := net.SplitHostPort(req.Host)
677 - if err != nil {
678 - host = req.Host // OK; there probably was no port
679 - }
707 + host := req.Host
708 prior, ok, omit = lastHeaderValue(req.Header, "X-Forwarded-Host")
709 if trusted && ok && prior != "" {
710 host = prior
@@ -691,7 +719,7 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
719 // reverseProxy performs a round-trip to the given backend and processes the response with the client.
720 // (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
721 // Go standard library which was used as the foundation.)
694 -func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
722 +func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
723 _ = di.Upstream.Host.countRequest(1)
724 //nolint:errcheck
725 defer di.Upstream.Host.countRequest(-1)
@@ -798,17 +826,19 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, repl *
826 // we make some data available via request context to child routes
827 // so that they may inherit some options and functions from the
828 // handler, and be able to copy the response.
829 + // we use the original request here, so that any routes from 'next'
830 + // see the original request rather than the proxy cloned request.
831 hrc := &handleResponseContext{
832 handler: h,
833 response: res,
834 start: start,
835 logger: logger,
836 }
807 - ctx := req.Context()
837 + ctx := origReq.Context()
838 ctx = context.WithValue(ctx, proxyHandleResponseContextCtxKey, hrc)
839
840 // pass the request through the response handler routes
811 - routeErr := rh.Routes.Compile(next).ServeHTTP(rw, req.WithContext(ctx))
841 + routeErr := rh.Routes.Compile(next).ServeHTTP(rw, origReq.WithContext(ctx))
842
843 // if the response handler routes already finalized the response,
844 // we can return early. It should be finalized if the routes executed
modules/caddyhttp/rewrite/rewrite.go
+2 -2
@@ -106,7 +106,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
106 zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
107 )
108
109 - changed := rewr.rewrite(r, repl, logger)
109 + changed := rewr.Rewrite(r, repl)
110
111 if changed {
112 logger.Debug("rewrote request",
@@ -121,7 +121,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
121 // rewrite performs the rewrites on r using repl, which should
122 // have been obtained from r, but is passed in for efficiency.
123 // It returns true if any changes were made to r.
124 -func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.Logger) bool {
124 +func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
125 oldMethod := r.Method
126 oldURI := r.RequestURI
127
modules/caddyhttp/rewrite/rewrite_test.go
+1 -1
@@ -292,7 +292,7 @@ func TestRewrite(t *testing.T) {
292 rep.re = re
293 }
294
295 - changed := tc.rule.rewrite(tc.input, repl, nil)
295 + changed := tc.rule.Rewrite(tc.input, repl)
296
297 if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
298 t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
modules/caddyhttp/standard/imports.go
+1
@@ -15,6 +15,7 @@ import (
15 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
16 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
17 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
18 + _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/forwardauth"
19 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
20 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
21 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing"