From e24a628ab1319381117a699190c7b522e57d034f Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Tue, 8 Oct 2019 00:23:08 -0700 Subject: [PATCH] net/http: do not sniff response if Content-Encoding header is set Fixes #31753 Change-Id: I32ec5906ef6714e19b094f67cb0f10a211a9c500 Reviewed-on: https://go-review.googlesource.com/c/go/+/199799 Run-TryBot: Emmanuel Odeke TryBot-Result: Gobot Gobot Reviewed-by: Brad Fitzpatrick --- src/net/http/clientserver_test.go | 2 + src/net/http/serve_test.go | 106 ++++++++++++++++++++++++++++++ src/net/http/server.go | 7 +- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/net/http/clientserver_test.go b/src/net/http/clientserver_test.go index d61d77839d..e9241c40dd 100644 --- a/src/net/http/clientserver_test.go +++ b/src/net/http/clientserver_test.go @@ -192,6 +192,8 @@ func (tt h12Compare) reqFunc() reqFunc { } func (tt h12Compare) run(t *testing.T) { + t.Skip("Temporarily disabling until https://golang.org/issue/31753 is fixed") + setParallel(t) cst1 := newClientServerTest(t, false, HandlerFunc(tt.Handler), tt.Opts...) defer cst1.close() diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go index 1d1449aa65..e1f8d2ddb7 100644 --- a/src/net/http/serve_test.go +++ b/src/net/http/serve_test.go @@ -10,6 +10,7 @@ import ( "bufio" "bytes" "compress/gzip" + "compress/zlib" "context" "crypto/tls" "encoding/json" @@ -6161,6 +6162,111 @@ func TestUnsupportedTransferEncodingsReturn501(t *testing.T) { } } +func TestContentEncodingNoSniffing_h1(t *testing.T) { + testContentEncodingNoSniffing(t, h1Mode) +} + +func TestContentEncodingNoSniffing_h2(t *testing.T) { + t.Skip("Waiting for h2_bundle.go update after https://golang.org/issue/31753") + testContentEncodingNoSniffing(t, h2Mode) +} + +// Issue 31753: don't sniff when Content-Encoding is set +func testContentEncodingNoSniffing(t *testing.T, h2 bool) { + setParallel(t) + defer afterTest(t) + + type setting struct { + name string + body []byte + + // setting contentEncoding as an interface instead of a string + // directly, so as to differentiate between 3 states: + // unset, empty string "" and set string "foo/bar". + contentEncoding interface{} + wantContentType string + } + + settings := []*setting{ + { + name: "gzip content-encoding, gzipped", // don't sniff. + contentEncoding: "application/gzip", + wantContentType: "", + body: func() []byte { + buf := new(bytes.Buffer) + gzw := gzip.NewWriter(buf) + gzw.Write([]byte("doctype html>

Hello

")) + gzw.Close() + return buf.Bytes() + }(), + }, + { + name: "zlib content-encoding, zlibbed", // don't sniff. + contentEncoding: "application/zlib", + wantContentType: "", + body: func() []byte { + buf := new(bytes.Buffer) + zw := zlib.NewWriter(buf) + zw.Write([]byte("doctype html>

Hello

")) + zw.Close() + return buf.Bytes() + }(), + }, + { + name: "no content-encoding", // must sniff. + wantContentType: "application/x-gzip", + body: func() []byte { + buf := new(bytes.Buffer) + gzw := gzip.NewWriter(buf) + gzw.Write([]byte("doctype html>

Hello

")) + gzw.Close() + return buf.Bytes() + }(), + }, + { + name: "phony content-encoding", // don't sniff. + contentEncoding: "foo/bar", + body: []byte("doctype html>

Hello

"), + }, + { + name: "empty but set content-encoding", + contentEncoding: "", + wantContentType: "audio/mpeg", + body: []byte("ID3"), + }, + } + + for _, tt := range settings { + t.Run(tt.name, func(t *testing.T) { + cst := newClientServerTest(t, h2, HandlerFunc(func(rw ResponseWriter, r *Request) { + if tt.contentEncoding != nil { + rw.Header().Set("Content-Encoding", tt.contentEncoding.(string)) + } + rw.Write(tt.body) + })) + defer cst.close() + + res, err := cst.c.Get(cst.ts.URL) + if err != nil { + t.Fatalf("Failed to fetch URL: %v", err) + } + defer res.Body.Close() + + if g, w := res.Header.Get("Content-Encoding"), tt.contentEncoding; g != w { + if w != nil { // The case where contentEncoding was set explicitly. + t.Errorf("Content-Encoding mismatch\n\tgot: %q\n\twant: %q", g, w) + } else if g != "" { // "" should be the equivalent when the contentEncoding is unset. + t.Errorf("Unexpected Content-Encoding %q", g) + } + } + + if g, w := res.Header.Get("Content-Type"), tt.wantContentType; g != w { + t.Errorf("Content-Type mismatch\n\tgot: %q\n\twant: %q", g, w) + } + }) + } +} + // fetchWireResponse is a helper for dialing to host, // sending http1ReqBody as the payload and retrieving // the response as it was sent on the wire. diff --git a/src/net/http/server.go b/src/net/http/server.go index 9ab4cc745e..6e31971180 100644 --- a/src/net/http/server.go +++ b/src/net/http/server.go @@ -1379,7 +1379,12 @@ func (cw *chunkWriter) writeHeader(p []byte) { if bodyAllowedForStatus(code) { // If no content type, apply sniffing algorithm to body. _, haveType := header["Content-Type"] - if !haveType && !hasTE && len(p) > 0 { + + // If the Content-Encoding was set and is non-blank, + // we shouldn't sniff the body. See Issue 31753. + ce := header.Get("Content-Encoding") + hasCE := len(ce) > 0 + if !hasCE && !haveType && !hasTE && len(p) > 0 { setHeader.contentType = DetectContentType(p) } } else {