Skip to content

Commit 8648d02

Browse files
committed
net/http/httputil: add support for X-Forwarded-Proto, X-Forwarded-Host and an option to not trust forwarded headers in ReverseProxy
1 parent 71239b4 commit 8648d02

File tree

2 files changed

+89
-0
lines changed

2 files changed

+89
-0
lines changed

src/net/http/httputil/reverseproxy.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ type ReverseProxy struct {
7676
// If nil, the default is to log the provided error and return
7777
// a 502 Status Bad Gateway response.
7878
ErrorHandler func(http.ResponseWriter, *http.Request, error)
79+
80+
// TrustForwardedHeaders specifies if X-Forwarded-For,
81+
// X-Forwarded-Proto and X-Forwarded-Host headers comming from
82+
// the previous proxy must be trusted or not.
83+
// If true, existing values of X-Forwarded-Proto and
84+
// X-Forwarded-Host will be preserved, and the current client IP
85+
// will be appended to the list in X-Forwarded-For.
86+
// If false, values of these headers will be set regardless of
87+
// any existing value.
88+
TrustForwardedHeaders bool
7989
}
8090

8191
// A BufferPool is an interface for getting and returning temporary
@@ -236,6 +246,23 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
236246
outreq.Header.Set("Upgrade", reqUpType)
237247
}
238248

249+
if _, ok := outreq.Header["X-Forwarded-Proto"]; !ok || !p.TrustForwardedHeaders {
250+
proto := "https"
251+
if req.TLS == nil {
252+
proto = "http"
253+
}
254+
255+
outreq.Header.Set("X-Forwarded-Proto", proto)
256+
}
257+
258+
if _, ok := outreq.Header["X-Forwarded-Host"]; !ok || !p.TrustForwardedHeaders {
259+
outreq.Header.Set("X-Forwarded-Host", outreq.Host)
260+
}
261+
262+
if !p.TrustForwardedHeaders {
263+
outreq.Header.Del("X-Forwarded-For")
264+
}
265+
239266
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
240267
// If we aren't the first proxy retain prior
241268
// X-Forwarded-For information as a comma+space

src/net/http/httputil/reverseproxy_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,61 @@ func TestReverseProxyStripHeadersPresentInConnection(t *testing.T) {
236236
}
237237
}
238238

239+
func TestDontTrustForwardedHeaders(t *testing.T) {
240+
const prevForwardedFor = "client ip"
241+
const prevForwardedProto = "https"
242+
const prevForwardedHost = "example.com"
243+
const backendResponse = "I am the backend"
244+
const backendStatus = 404
245+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
246+
if r.Header.Get("X-Forwarded-For") == "" {
247+
t.Errorf("didn't get X-Forwarded-For header")
248+
}
249+
if strings.Contains(r.Header.Get("X-Forwarded-For"), prevForwardedFor) {
250+
t.Errorf("X-Forwarded-For contains prior data")
251+
}
252+
if strings.Contains(r.Header.Get("X-Forwarded-Proto"), prevForwardedProto) {
253+
t.Errorf("X-Forwarded-Proto contains prior data")
254+
}
255+
if strings.Contains(r.Header.Get("X-Forwarded-Host"), prevForwardedHost) {
256+
t.Errorf("X-Forwarded-Host contains prior data")
257+
}
258+
w.WriteHeader(backendStatus)
259+
w.Write([]byte(backendResponse))
260+
}))
261+
defer backend.Close()
262+
backendURL, err := url.Parse(backend.URL)
263+
if err != nil {
264+
t.Fatal(err)
265+
}
266+
proxyHandler := NewSingleHostReverseProxy(backendURL)
267+
frontend := httptest.NewServer(proxyHandler)
268+
defer frontend.Close()
269+
270+
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
271+
getReq.Host = "some-name"
272+
getReq.Header.Set("Connection", "close")
273+
getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
274+
getReq.Header.Set("X-Forwarded-Proto", prevForwardedProto)
275+
getReq.Header.Set("X-Forwarded-Host", prevForwardedHost)
276+
getReq.Close = true
277+
res, err := frontend.Client().Do(getReq)
278+
if err != nil {
279+
t.Fatalf("Get: %v", err)
280+
}
281+
if g, e := res.StatusCode, backendStatus; g != e {
282+
t.Errorf("got res.StatusCode %d; expected %d", g, e)
283+
}
284+
bodyBytes, _ := ioutil.ReadAll(res.Body)
285+
if g, e := string(bodyBytes), backendResponse; g != e {
286+
t.Errorf("got body %q; expected %q", g, e)
287+
}
288+
}
289+
239290
func TestXForwardedFor(t *testing.T) {
240291
const prevForwardedFor = "client ip"
292+
const prevForwardedProto = "https"
293+
const prevForwardedHost = "example.com"
241294
const backendResponse = "I am the backend"
242295
const backendStatus = 404
243296
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -247,6 +300,12 @@ func TestXForwardedFor(t *testing.T) {
247300
if !strings.Contains(r.Header.Get("X-Forwarded-For"), prevForwardedFor) {
248301
t.Errorf("X-Forwarded-For didn't contain prior data")
249302
}
303+
if !strings.Contains(r.Header.Get("X-Forwarded-Proto"), prevForwardedProto) {
304+
t.Errorf("X-Forwarded-Proto didn't contain prior data")
305+
}
306+
if !strings.Contains(r.Header.Get("X-Forwarded-Host"), prevForwardedHost) {
307+
t.Errorf("X-Forwarded-Host didn't contain prior data")
308+
}
250309
w.WriteHeader(backendStatus)
251310
w.Write([]byte(backendResponse))
252311
}))
@@ -256,13 +315,16 @@ func TestXForwardedFor(t *testing.T) {
256315
t.Fatal(err)
257316
}
258317
proxyHandler := NewSingleHostReverseProxy(backendURL)
318+
proxyHandler.TrustForwardedHeaders = true
259319
frontend := httptest.NewServer(proxyHandler)
260320
defer frontend.Close()
261321

262322
getReq, _ := http.NewRequest("GET", frontend.URL, nil)
263323
getReq.Host = "some-name"
264324
getReq.Header.Set("Connection", "close")
265325
getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
326+
getReq.Header.Set("X-Forwarded-Proto", prevForwardedProto)
327+
getReq.Header.Set("X-Forwarded-Host", prevForwardedHost)
266328
getReq.Close = true
267329
res, err := frontend.Client().Do(getReq)
268330
if err != nil {

0 commit comments

Comments
 (0)