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, 21:45 UTC
reverseproxy: Make shallow-ish clone of the request (#4551)
* reverseproxy: Make shallow-ish clone of the request * Refactor request cloning into separate function Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
Francis Lavoie committed
Mar 3, 2022 at 11:54 UTC
f5e104944ea48e776580a0a9ebbd02fca33d659f
1 file changed
+76
-61
modules/caddyhttp/reverseproxy/reverseproxy.go
+76
-61
@@ -358,54 +358,20 @@ func (h *Handler) Cleanup() error {
358
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
359
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
360
361
- // if enabled, buffer client request;
362
- // this should only be enabled if the
363
- // upstream requires it and does not
364
- // work with "slow clients" (gunicorn,
365
- // etc.) - this obviously has a perf
366
- // overhead and makes the proxy at
367
- // risk of exhausting memory and more
368
- // susceptible to slowloris attacks,
369
- // so it is strongly recommended to
370
- // only use this feature if absolutely
371
- // required, if read timeouts are set,
372
- // and if body size is limited
373
- if h.BufferRequests {
374
- r.Body = h.bufferedBody(r.Body)
375
- }
376
-
361
// prepare the request for proxying; this is needed only once
378
- err := h.prepareRequest(r)
362
+ clonedReq, err := h.prepareRequest(r)
363
if err != nil {
364
return caddyhttp.Error(http.StatusInternalServerError,
365
fmt.Errorf("preparing request for upstream round-trip: %v", err))
366
}
367
368
// we will need the original headers and Host value if
385
- // header operations are configured; and we should
386
- // restore them after we're done if they are changed
387
- // (for example, changing the outbound Host header
388
- // should not permanently change r.Host; issue #3509)
389
- reqHost := r.Host
390
- reqHeader := r.Header
391
-
392
- // sanitize the request URL; we expect it to not contain the scheme and host
393
- // since those should be determined by r.TLS and r.Host respectively, but
394
- // some clients may include it in the request-line, which is technically
395
- // valid in HTTP, but breaks reverseproxy behaviour, overriding how the
396
- // dialer will behave. See #4237 for context.
397
- origURLScheme := r.URL.Scheme
398
- origURLHost := r.URL.Host
399
- r.URL.Scheme = ""
400
- r.URL.Host = ""
401
-
402
- // restore modifications to the request after we're done proxying
403
- defer func() {
404
- r.Host = reqHost // TODO: data race, see #4038
405
- r.Header = reqHeader // TODO: data race, see #4038
406
- r.URL.Scheme = origURLScheme
407
- r.URL.Host = origURLHost
408
- }()
369
+ // header operations are configured; this is so that each
370
+ // retry can apply the modifications, because placeholders
371
+ // may be used which depend on the selected upstream for
372
+ // their values
373
+ reqHost := clonedReq.Host
374
+ reqHeader := clonedReq.Header
375
376
start := time.Now()
377
defer func() {
@@ -416,12 +382,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
382
var proxyErr error
383
for {
384
// choose an available upstream
419
- upstream := h.LoadBalancing.SelectionPolicy.Select(h.Upstreams, r, w)
385
+ upstream := h.LoadBalancing.SelectionPolicy.Select(h.Upstreams, clonedReq, w)
386
if upstream == nil {
387
if proxyErr == nil {
388
proxyErr = fmt.Errorf("no upstreams available")
389
}
424
- if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, r) {
390
+ if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, clonedReq) {
391
break
392
}
393
continue
@@ -430,7 +396,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
396
// the dial address may vary per-request if placeholders are
397
// used, so perform those replacements here; the resulting
398
// DialInfo struct should have valid network address syntax
433
- dialInfo, err := upstream.fillDialInfo(r)
399
+ dialInfo, err := upstream.fillDialInfo(clonedReq)
400
if err != nil {
401
return statusError(fmt.Errorf("making dial info: %v", err))
402
}
@@ -451,17 +417,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
417
418
// mutate request headers according to this upstream;
419
// because we're in a retry loop, we have to copy
454
- // headers (and the r.Host value) from the original
420
+ // headers (and the Host value) from the original
421
// so that each retry is identical to the first
422
if h.Headers != nil && h.Headers.Request != nil {
457
- r.Header = make(http.Header)
458
- copyHeader(r.Header, reqHeader)
459
- r.Host = reqHost
460
- h.Headers.Request.ApplyToRequest(r)
423
+ clonedReq.Header = make(http.Header)
424
+ copyHeader(clonedReq.Header, reqHeader)
425
+ clonedReq.Host = reqHost
426
+ h.Headers.Request.ApplyToRequest(clonedReq)
427
}
428
429
// proxy the request to that upstream
464
- proxyErr = h.reverseProxy(w, r, repl, dialInfo, next)
430
+ proxyErr = h.reverseProxy(w, clonedReq, repl, dialInfo, next)
431
if proxyErr == nil || proxyErr == context.Canceled {
432
// context.Canceled happens when the downstream client
433
// cancels the request, which is not our failure
@@ -480,7 +446,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
446
h.countFailure(upstream)
447
448
// if we've tried long enough, break
483
- if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, r) {
449
+ if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, clonedReq) {
450
break
451
}
452
}
@@ -488,14 +454,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
454
return statusError(proxyErr)
455
}
456
491
-// prepareRequest modifies req so that it is ready to be proxied,
492
-// except for directing to a specific upstream. This method mutates
493
-// headers and other necessary properties of the request and should
494
-// be done just once (before proxying) regardless of proxy retries.
495
-// This assumes that no mutations of the request are performed
496
-// by h during or after proxying.
497
-func (h Handler) prepareRequest(req *http.Request) error {
498
- // most of this is borrowed from the Go std lib reverse proxy
457
+// prepareRequest clones req so that it can be safely modified without
458
+// changing the original request or introducing data races. It then
459
+// modifies it so that it is ready to be proxied, except for directing
460
+// to a specific upstream. This method adjusts headers and other relevant
461
+// properties of the cloned request and should be done just once (before
462
+// proxying) regardless of proxy retries. This assumes that no mutations
463
+// of the cloned request are performed by h during or after proxying.
464
+func (h Handler) prepareRequest(req *http.Request) (*http.Request, error) {
465
+ req = cloneRequest(req)
466
+
467
+ // if enabled, buffer client request; this should only be
468
+ // enabled if the upstream requires it and does not work
469
+ // with "slow clients" (gunicorn, etc.) - this obviously
470
+ // has a perf overhead and makes the proxy at risk of
471
+ // exhausting memory and more susceptible to slowloris
472
+ // attacks, so it is strongly recommended to only use this
473
+ // feature if absolutely required, if read timeouts are
474
+ // set, and if body size is limited
475
+ if h.BufferRequests {
476
+ req.Body = h.bufferedBody(req.Body)
477
+ }
478
479
if req.ContentLength == 0 {
480
req.Body = nil // Issue golang/go#16036: nil Body for http.Transport retries
@@ -560,7 +539,7 @@ func (h Handler) prepareRequest(req *http.Request) error {
539
req.Header.Set("X-Forwarded-Proto", proto)
540
}
541
563
- return nil
542
+ return req, nil
543
}
544
545
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
@@ -807,7 +786,7 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, proxyErr er
786
787
// directRequest modifies only req.URL so that it points to the upstream
788
// in the given DialInfo. It must modify ONLY the request URL.
810
-func (h Handler) directRequest(req *http.Request, di DialInfo) {
789
+func (Handler) directRequest(req *http.Request, di DialInfo) {
790
// we need a host, so set the upstream's host address
791
reqHost := di.Address
792
@@ -845,6 +824,42 @@ func (h Handler) bufferedBody(originalBody io.ReadCloser) io.ReadCloser {
824
}
825
}
826
827
+// cloneRequest makes a semi-deep clone of origReq.
828
+//
829
+// Most of this code is borrowed from the Go stdlib reverse proxy,
830
+// but we make a shallow-ish clone the request (deep clone only
831
+// the headers and URL) so we can avoid manipulating the original
832
+// request when using it to proxy upstream. This prevents request
833
+// corruption and data races.
834
+func cloneRequest(origReq *http.Request) *http.Request {
835
+ req := new(http.Request)
836
+ *req = *origReq
837
+ if origReq.URL != nil {
838
+ newURL := new(url.URL)
839
+ *newURL = *origReq.URL
840
+ if origReq.URL.User != nil {
841
+ newURL.User = new(url.Userinfo)
842
+ *newURL.User = *origReq.URL.User
843
+ }
844
+ // sanitize the request URL; we expect it to not contain the
845
+ // scheme and host since those should be determined by r.TLS
846
+ // and r.Host respectively, but some clients may include it
847
+ // in the request-line, which is technically valid in HTTP,
848
+ // but breaks reverseproxy behaviour, overriding how the
849
+ // dialer will behave. See #4237 for context.
850
+ newURL.Scheme = ""
851
+ newURL.Host = ""
852
+ req.URL = newURL
853
+ }
854
+ if origReq.Header != nil {
855
+ req.Header = origReq.Header.Clone()
856
+ }
857
+ if origReq.Trailer != nil {
858
+ req.Trailer = origReq.Trailer.Clone()
859
+ }
860
+ return req
861
+}
862
+
863
func copyHeader(dst, src http.Header) {
864
for k, vv := range src {
865
for _, v := range vv {