Skip to content

Commit a557938

Browse files
committed
net/http/httputil: add ReverseProxy.Rewrite
Add a new Rewrite hook to ReverseProxy, superseding the Director hook. Director does not distinguish between the inbound and outbound request, which makes it possible for headers added by Director to be inadvertently removed before forwarding if they are listed in the inbound request's Connection header. Rewrite accepts a value containing the inbound and outbound requests, with hop-by-hop headers already removed from the outbound request, avoiding this problem. ReverseProxy's appends the client IP to the inbound X-Forwarded-For header by default. Users must manually delete untrusted X-Forwarded-For values. When used with a Rewrite hook, ReverseProxy now strips X-Forwarded-* headers by default. NewSingleHostReverseProxy creates a proxy that does not rewrite the Host header of inbound requests. Changing this behavior is cumbersome, as it requires wrapping the Director function created by NewSingleHostReverseProxy. The Rewrite hook's ProxyRequest parameter provides a SetURL method that provides equivalent functionality to NewSingleHostReverseProxy, rewrites the Host header by default, and can be more easily extended with additional customizations. Fixes #28168. Fixes #50580. Fixes #53002. Change-Id: Ib84e2fdd1d52c610e3887af66f517d4a74e594d0 Reviewed-on: https://go-review.googlesource.com/c/go/+/407214 Reviewed-by: Brad Fitzpatrick <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Run-TryBot: Damien Neil <[email protected]>
1 parent 6800559 commit a557938

File tree

4 files changed

+304
-66
lines changed

4 files changed

+304
-66
lines changed

api/next/53002.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pkg net/http/httputil, method (*ProxyRequest) SetURL(*url.URL) #53002
2+
pkg net/http/httputil, method (*ProxyRequest) SetXForwarded() #53002
3+
pkg net/http/httputil, type ProxyRequest struct #53002
4+
pkg net/http/httputil, type ProxyRequest struct, In *http.Request #53002
5+
pkg net/http/httputil, type ProxyRequest struct, Out *http.Request #53002
6+
pkg net/http/httputil, type ReverseProxy struct, Rewrite func(*ProxyRequest) #53002

src/net/http/httputil/example_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,12 @@ func ExampleReverseProxy() {
103103
if err != nil {
104104
log.Fatal(err)
105105
}
106-
frontendProxy := httptest.NewServer(httputil.NewSingleHostReverseProxy(rpURL))
106+
frontendProxy := httptest.NewServer(&httputil.ReverseProxy{
107+
Rewrite: func(r *httputil.ProxyRequest) {
108+
r.SetXForwarded()
109+
r.SetURL(rpURL)
110+
},
111+
})
107112
defer frontendProxy.Close()
108113

109114
resp, err := http.Get(frontendProxy.URL)

src/net/http/httputil/reverseproxy.go

Lines changed: 193 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package httputil
88

99
import (
1010
"context"
11+
"errors"
1112
"fmt"
1213
"io"
1314
"log"
@@ -24,33 +25,118 @@ import (
2425
"golang.org/x/net/http/httpguts"
2526
)
2627

27-
// ReverseProxy is an HTTP Handler that takes an incoming request and
28-
// sends it to another server, proxying the response back to the
29-
// client.
28+
// A ProxyRequest contains a request to be rewritten by a ReverseProxy.
29+
type ProxyRequest struct {
30+
// In is the request received by the proxy.
31+
// The Rewrite function must not modify In.
32+
In *http.Request
33+
34+
// Out is the request which will be sent by the proxy.
35+
// The Rewrite function may modify or replace this request.
36+
// Hop-by-hop headers are removed from this request
37+
// before Rewrite is called.
38+
Out *http.Request
39+
}
40+
41+
// SetURL routes the outbound request to the scheme, host, and base path
42+
// provided in target. If the target's path is "/base" and the incoming
43+
// request was for "/dir", the target request will be for "/base/dir".
3044
//
31-
// ReverseProxy by default sets
32-
// - the X-Forwarded-For header to the client IP address;
33-
// - the X-Forwarded-Host header to the host of the original client
34-
// request; and
35-
// - the X-Forwarded-Proto header to "https" if the client request
36-
// was made on a TLS-enabled connection or "http" otherwise.
45+
// SetURL rewrites the outbound Host header to match the target's host.
46+
// To preserve the inbound request's Host header (the default behavior
47+
// of NewSingleHostReverseProxy):
3748
//
38-
// If an X-Forwarded-For header already exists, the client IP is
39-
// appended to the existing values.
49+
// rewriteFunc := func(r *httputil.ProxyRequest) {
50+
// r.SetURL(url)
51+
// r.Out.Host = r.In.Host
52+
// }
53+
func (r *ProxyRequest) SetURL(target *url.URL) {
54+
rewriteRequestURL(r.Out, target)
55+
r.Out.Host = ""
56+
}
57+
58+
// SetXForwarded sets the X-Forwarded-For, X-Forwarded-Host, and
59+
// X-Forwarded-Proto headers of the outbound request.
4060
//
41-
// If a header exists in the Request.Header map but has a nil value
42-
// (such as when set by the Director func), it is not modified.
61+
// - The X-Forwarded-For header is set to the client IP address.
62+
// - The X-Forwarded-Host header is set to the host name requested
63+
// by the client.
64+
// - The X-Forwarded-Proto header is set to "http" or "https", depending
65+
// on whether the inbound request was made on a TLS-enabled connection.
4366
//
44-
// To prevent IP spoofing, be sure to delete any pre-existing
45-
// X-Forwarded-For header coming from the client or
46-
// an untrusted proxy.
67+
// If the outbound request contains an existing X-Forwarded-For header,
68+
// SetXForwarded appends the client IP address to it. To append to the
69+
// inbound request's X-Forwarded-For header (the default behavior of
70+
// ReverseProxy when using a Director function), copy the header
71+
// from the inbound request before calling SetXForwarded:
72+
//
73+
// rewriteFunc := func(r *httputil.ProxyRequest) {
74+
// r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"]
75+
// r.SetXForwarded()
76+
// }
77+
func (r *ProxyRequest) SetXForwarded() {
78+
clientIP, _, err := net.SplitHostPort(r.In.RemoteAddr)
79+
if err == nil {
80+
prior := r.Out.Header["X-Forwarded-For"]
81+
if len(prior) > 0 {
82+
clientIP = strings.Join(prior, ", ") + ", " + clientIP
83+
}
84+
r.Out.Header.Set("X-Forwarded-For", clientIP)
85+
} else {
86+
r.Out.Header.Del("X-Forwarded-For")
87+
}
88+
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
89+
if r.In.TLS == nil {
90+
r.Out.Header.Set("X-Forwarded-Proto", "http")
91+
} else {
92+
r.Out.Header.Set("X-Forwarded-Proto", "https")
93+
}
94+
}
95+
96+
// ReverseProxy is an HTTP Handler that takes an incoming request and
97+
// sends it to another server, proxying the response back to the
98+
// client.
4799
type ReverseProxy struct {
48-
// Director must be a function which modifies
100+
// Rewrite must be a function which modifies
101+
// the request into a new request to be sent
102+
// using Transport. Its response is then copied
103+
// back to the original client unmodified.
104+
// Rewrite must not access the provided ProxyRequest
105+
// or its contents after returning.
106+
//
107+
// The Forwarded, X-Forwarded, X-Forwarded-Host,
108+
// and X-Forwarded-Proto headers are removed from the
109+
// outbound request before Rewrite is called. See also
110+
// the ProxyRequest.SetXForwarded method.
111+
//
112+
// At most one of Rewrite or Director may be set.
113+
Rewrite func(*ProxyRequest)
114+
115+
// Director is a function which modifies the
49116
// the request into a new request to be sent
50117
// using Transport. Its response is then copied
51118
// back to the original client unmodified.
52119
// Director must not access the provided Request
53120
// after returning.
121+
//
122+
// By default, the X-Forwarded-For, X-Forwarded-Host, and
123+
// X-Forwarded-Proto headers of the ourgoing request are
124+
// set as by the ProxyRequest.SetXForwarded function.
125+
//
126+
// If an X-Forwarded-For header already exists, the client IP is
127+
// appended to the existing values. To prevent IP spoofing, be
128+
// sure to delete any pre-existing X-Forwarded-For header
129+
// coming from the client or an untrusted proxy.
130+
//
131+
// If a header exists in the Request.Header map but has a nil value
132+
// (such as when set by the Director func), it is not modified.
133+
//
134+
// Hop-by-hop headers are removed from the request after
135+
// Director returns, which can remove headers added by
136+
// Director. Use a Rewrite function instead to ensure
137+
// modifications to the request are preserved.
138+
//
139+
// At most one of Rewrite or Director may be set.
54140
Director func(*http.Request)
55141

56142
// The transport used to perform proxy requests.
@@ -142,24 +228,41 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
142228
// URLs to the scheme, host, and base path provided in target. If the
143229
// target's path is "/base" and the incoming request was for "/dir",
144230
// the target request will be for /base/dir.
231+
//
145232
// NewSingleHostReverseProxy does not rewrite the Host header.
146-
// To rewrite Host headers, use ReverseProxy directly with a custom
147-
// Director policy.
233+
//
234+
// To customize the ReverseProxy behavior beyond what
235+
// NewSingleHostReverseProxy provides, use ReverseProxy directly
236+
// with a Rewrite function. The ProxyRequest SetURL method
237+
// may be used to route the outbound request. (Note that SetURL,
238+
// unlike NewSingleHostReverseProxy, rewrites the Host header
239+
// of the outbound request by default.)
240+
//
241+
// proxy := &ReverseProxy{
242+
// Rewrite: func(r *ProxyRequest) {
243+
// r.SetURL(target)
244+
// r.Out.Host = r.In.Host // if desired
245+
// }
246+
// }
148247
func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
149-
targetQuery := target.RawQuery
150248
director := func(req *http.Request) {
151-
req.URL.Scheme = target.Scheme
152-
req.URL.Host = target.Host
153-
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
154-
if targetQuery == "" || req.URL.RawQuery == "" {
155-
req.URL.RawQuery = targetQuery + req.URL.RawQuery
156-
} else {
157-
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
158-
}
249+
rewriteRequestURL(req, target)
159250
}
160251
return &ReverseProxy{Director: director}
161252
}
162253

254+
func rewriteRequestURL(req *http.Request, target *url.URL) {
255+
targetQuery := target.RawQuery
256+
req.URL.Scheme = target.Scheme
257+
req.URL.Host = target.Host
258+
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
259+
if targetQuery == "" || req.URL.RawQuery == "" {
260+
req.URL.RawQuery = targetQuery + req.URL.RawQuery
261+
} else {
262+
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
263+
}
264+
}
265+
163266
func copyHeader(dst, src http.Header) {
164267
for k, vv := range src {
165268
for _, v := range vv {
@@ -260,28 +363,28 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
260363
outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
261364
}
262365

263-
p.Director(outreq)
366+
if (p.Director != nil) == (p.Rewrite != nil) {
367+
p.getErrorHandler()(rw, req, errors.New("ReverseProxy must have exactly one of Director or Rewrite set"))
368+
return
369+
}
370+
371+
if p.Director != nil {
372+
p.Director(outreq)
373+
}
264374
outreq.Close = false
265375

266376
reqUpType := upgradeType(outreq.Header)
267377
if !ascii.IsPrint(reqUpType) {
268378
p.getErrorHandler()(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
269379
return
270380
}
271-
removeConnectionHeaders(outreq.Header)
272-
273-
// Remove hop-by-hop headers to the backend. Especially
274-
// important is "Connection" because we want a persistent
275-
// connection, regardless of what the client sent to us.
276-
for _, h := range hopHeaders {
277-
outreq.Header.Del(h)
278-
}
381+
removeHopByHopHeaders(outreq.Header)
279382

280383
// Issue 21096: tell backend applications that care about trailer support
281384
// that we support trailers. (We do, but we don't go out of our way to
282385
// advertise that unless the incoming client request thought it was worth
283386
// mentioning.) Note that we look at req.Header, not outreq.Header, since
284-
// the latter has passed through removeConnectionHeaders.
387+
// the latter has passed through removeHopByHopHeaders.
285388
if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
286389
outreq.Header.Set("Te", "trailers")
287390
}
@@ -293,27 +396,44 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
293396
outreq.Header.Set("Upgrade", reqUpType)
294397
}
295398

296-
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
297-
// If we aren't the first proxy retain prior
298-
// X-Forwarded-For information as a comma+space
299-
// separated list and fold multiple headers into one.
300-
prior, ok := outreq.Header["X-Forwarded-For"]
301-
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
302-
if len(prior) > 0 {
303-
clientIP = strings.Join(prior, ", ") + ", " + clientIP
399+
if p.Rewrite != nil {
400+
// Strip client-provided forwarding headers.
401+
// The Rewrite func may use SetXForwarded to set new values
402+
// for these or copy the previous values from the inbound request.
403+
outreq.Header.Del("Forwarded")
404+
outreq.Header.Del("X-Forwarded-For")
405+
outreq.Header.Del("X-Forwarded-Host")
406+
outreq.Header.Del("X-Forwarded-Proto")
407+
408+
pr := &ProxyRequest{
409+
In: req,
410+
Out: outreq,
304411
}
305-
if !omit {
306-
outreq.Header.Set("X-Forwarded-For", clientIP)
412+
p.Rewrite(pr)
413+
outreq = pr.Out
414+
} else {
415+
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
416+
// If we aren't the first proxy retain prior
417+
// X-Forwarded-For information as a comma+space
418+
// separated list and fold multiple headers into one.
419+
prior, ok := outreq.Header["X-Forwarded-For"]
420+
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
421+
if len(prior) > 0 {
422+
clientIP = strings.Join(prior, ", ") + ", " + clientIP
423+
}
424+
if !omit {
425+
outreq.Header.Set("X-Forwarded-For", clientIP)
426+
}
307427
}
308-
}
309-
if prior, ok := outreq.Header["X-Forwarded-Host"]; !(ok && prior == nil) {
310-
outreq.Header.Set("X-Forwarded-Host", req.Host)
311-
}
312-
if prior, ok := outreq.Header["X-Forwarded-Proto"]; !(ok && prior == nil) {
313-
if req.TLS == nil {
314-
outreq.Header.Set("X-Forwarded-Proto", "http")
315-
} else {
316-
outreq.Header.Set("X-Forwarded-Proto", "https")
428+
if prior, ok := outreq.Header["X-Forwarded-Host"]; !(ok && prior == nil) {
429+
outreq.Header.Set("X-Forwarded-Host", req.Host)
430+
}
431+
if prior, ok := outreq.Header["X-Forwarded-Proto"]; !(ok && prior == nil) {
432+
if req.TLS == nil {
433+
outreq.Header.Set("X-Forwarded-Proto", "http")
434+
} else {
435+
outreq.Header.Set("X-Forwarded-Proto", "https")
436+
}
317437
}
318438
}
319439

@@ -323,6 +443,12 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
323443
outreq.Header.Set("User-Agent", "")
324444
}
325445

446+
if _, ok := outreq.Header["User-Agent"]; !ok {
447+
// If the outbound request doesn't have a User-Agent header set,
448+
// don't send the default Go HTTP client User-Agent.
449+
outreq.Header.Set("User-Agent", "")
450+
}
451+
326452
res, err := transport.RoundTrip(outreq)
327453
if err != nil {
328454
p.getErrorHandler()(rw, outreq, err)
@@ -338,11 +464,7 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
338464
return
339465
}
340466

341-
removeConnectionHeaders(res.Header)
342-
343-
for _, h := range hopHeaders {
344-
res.Header.Del(h)
345-
}
467+
removeHopByHopHeaders(res.Header)
346468

347469
if !p.modifyResponse(rw, res, outreq) {
348470
return
@@ -421,16 +543,22 @@ func shouldPanicOnCopyError(req *http.Request) bool {
421543
return false
422544
}
423545

424-
// removeConnectionHeaders removes hop-by-hop headers listed in the "Connection" header of h.
425-
// See RFC 7230, section 6.1
426-
func removeConnectionHeaders(h http.Header) {
546+
// removeHopByHopHeaders removes hop-by-hop headers.
547+
func removeHopByHopHeaders(h http.Header) {
548+
// RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
427549
for _, f := range h["Connection"] {
428550
for _, sf := range strings.Split(f, ",") {
429551
if sf = textproto.TrimString(sf); sf != "" {
430552
h.Del(sf)
431553
}
432554
}
433555
}
556+
// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
557+
// This behavior is superseded by the RFC 7230 Connection header, but
558+
// preserve it for backwards compatibility.
559+
for _, f := range hopHeaders {
560+
h.Del(f)
561+
}
434562
}
435563

436564
// flushInterval returns the p.FlushInterval value, conditionally

0 commit comments

Comments
 (0)