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"