diff --git a/caddyhttp/proxy/proxy.go b/caddyhttp/proxy/proxy.go index fe959791..0f48a61f 100644 --- a/caddyhttp/proxy/proxy.go +++ b/caddyhttp/proxy/proxy.go @@ -247,12 +247,28 @@ func createUpstreamRequest(r *http.Request) *http.Request { outreq.URL.Opaque = outreq.URL.RawPath } + // We are modifying the same underlying map from req (shallow + // copied above) so we only copy it if necessary. + copiedHeaders := false + + // Remove hop-by-hop headers listed in the "Connection" header. + // See RFC 2616, section 14.10. + if c := outreq.Header.Get("Connection"); c != "" { + for _, f := range strings.Split(c, ",") { + if f = strings.TrimSpace(f); f != "" { + if !copiedHeaders { + outreq.Header = make(http.Header) + copyHeader(outreq.Header, r.Header) + copiedHeaders = true + } + outreq.Header.Del(f) + } + } + } + // Remove hop-by-hop headers to the backend. Especially // important is "Connection" because we want a persistent - // connection, regardless of what the client sent to us. This - // is modifying the same underlying map from r (shallow - // copied above) so we only copy it if necessary. - var copiedHeaders bool + // connection, regardless of what the client sent to us. for _, h := range hopHeaders { if outreq.Header.Get(h) != "" { if !copiedHeaders { diff --git a/caddyhttp/proxy/proxy_test.go b/caddyhttp/proxy/proxy_test.go index 85897c00..686a79c5 100644 --- a/caddyhttp/proxy/proxy_test.go +++ b/caddyhttp/proxy/proxy_test.go @@ -42,10 +42,32 @@ func TestReverseProxy(t *testing.T) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) + verifyHeaders := func(headers http.Header, trailers http.Header) { + if headers.Get("X-Header") != "header-value" { + t.Error("Expected header 'X-Header' to be proxied properly") + } + + if trailers == nil { + t.Error("Expected to receive trailers") + } + if trailers.Get("X-Trailer") != "trailer-value" { + t.Error("Expected header 'X-Trailer' to be proxied properly") + } + } + var requestReceived bool backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // read the body (even if it's empty) to make Go parse trailers + io.Copy(ioutil.Discard, r.Body) + verifyHeaders(r.Header, r.Trailer) + requestReceived = true + + w.Header().Set("Trailer", "X-Trailer") + w.Header().Set("X-Header", "header-value") + w.WriteHeader(http.StatusOK) w.Write([]byte("Hello, client")) + w.Header().Set("X-Trailer", "trailer-value") })) defer backend.Close() @@ -59,12 +81,21 @@ func TestReverseProxy(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) w := httptest.NewRecorder() + r.ContentLength = -1 // force chunked encoding (required for trailers) + r.Header.Set("X-Header", "header-value") + r.Trailer = map[string][]string{ + "X-Trailer": {"trailer-value"}, + } + p.ServeHTTP(w, r) if !requestReceived { t.Error("Expected backend to receive request, but it didn't") } + res := w.Result() + verifyHeaders(res.Header, res.Trailer) + // Make sure {upstream} placeholder is set rr := httpserver.NewResponseRecorder(httptest.NewRecorder()) rr.Replacer = httpserver.NewReplacer(r, rr, "-") diff --git a/caddyhttp/proxy/reverseproxy.go b/caddyhttp/proxy/reverseproxy.go index 552c1ab9..a59f4bc8 100644 --- a/caddyhttp/proxy/reverseproxy.go +++ b/caddyhttp/proxy/reverseproxy.go @@ -211,10 +211,27 @@ func (rp *ReverseProxy) ServeHTTP(rw http.ResponseWriter, outreq *http.Request, return err } + isWebsocket := res.StatusCode == http.StatusSwitchingProtocols && strings.ToLower(res.Header.Get("Upgrade")) == "websocket" + + // Remove hop-by-hop headers listed in the + // "Connection" header of the response. + if c := res.Header.Get("Connection"); c != "" { + for _, f := range strings.Split(c, ",") { + if f = strings.TrimSpace(f); f != "" { + res.Header.Del(f) + } + } + } + + for _, h := range hopHeaders { + res.Header.Del(h) + } + if respUpdateFn != nil { respUpdateFn(res) } - if res.StatusCode == http.StatusSwitchingProtocols && strings.ToLower(res.Header.Get("Upgrade")) == "websocket" { + + if isWebsocket { res.Body.Close() hj, ok := rw.(http.Hijacker) if !ok { @@ -246,13 +263,30 @@ func (rp *ReverseProxy) ServeHTTP(rw http.ResponseWriter, outreq *http.Request, go pooledIoCopy(backendConn, conn) // write tcp stream to backend pooledIoCopy(conn, backendConn) // read tcp stream from backend } else { - defer res.Body.Close() - for _, h := range hopHeaders { - res.Header.Del(h) - } copyHeader(rw.Header(), res.Header) + + // The "Trailer" header isn't included in the Transport's response, + // at least for *http.Transport. Build it up from Trailer. + if len(res.Trailer) > 0 { + trailerKeys := make([]string, 0, len(res.Trailer)) + for k := range res.Trailer { + trailerKeys = append(trailerKeys, k) + } + rw.Header().Add("Trailer", strings.Join(trailerKeys, ", ")) + } + rw.WriteHeader(res.StatusCode) + if len(res.Trailer) > 0 { + // Force chunking if we saw a response trailer. + // This prevents net/http from calculating the length for short + // bodies and adding a Content-Length. + if fl, ok := rw.(http.Flusher); ok { + fl.Flush() + } + } rp.copyResponse(rw, res.Body) + res.Body.Close() // close now, instead of defer, to populate res.Trailer + copyHeader(rw.Header(), res.Trailer) } return nil @@ -305,16 +339,17 @@ func copyHeader(dst, src http.Header) { // Hop-by-hop headers. These are removed when sent to the backend. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html var hopHeaders = []string{ + "Alt-Svc", + "Alternate-Protocol", "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", - "Te", // canonicalized version of "TE" - "Trailers", + "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google + "Te", // canonicalized version of "TE" + "Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522 "Transfer-Encoding", "Upgrade", - "Alternate-Protocol", - "Alt-Svc", } type respUpdateFn func(resp *http.Response)