]> Cypherpunks repositories - gostls13.git/commitdiff
net/http: restore Transport's Request.Body byte sniff in limited cases
authorBrad Fitzpatrick <bradfitz@golang.org>
Thu, 1 Dec 2016 21:45:59 +0000 (21:45 +0000)
committerBrad Fitzpatrick <bradfitz@golang.org>
Thu, 22 Dec 2016 19:56:51 +0000 (19:56 +0000)
In Go 1.8, we'd removed the Transport's Request.Body
one-byte-Read-sniffing to disambiguate between non-nil Request.Body
with a ContentLength of 0 or -1. Previously, we tried to see whether a
ContentLength of 0 meant actually zero, or just an unset by reading a
single byte of the Request.Body and then stitching any read byte back
together with the original Request.Body.

That historically has caused many problems due to either data races,
blocking forever (#17480), or losing bytes (#17071). Thus, we removed
it in both HTTP/1 and HTTP/2 in Go 1.8. Unfortunately, during the Go
1.8 beta, we've found that a few people have gotten bitten by the
behavior change on requests with methods typically not containing
request bodies (e.g. GET, HEAD, DELETE). The most popular example is
the aws-go SDK, which always set http.Request.Body to a non-nil value,
even on such request methods. That was causing Go 1.8 to send such
requests with Transfer-Encoding chunked bodies, with zero bytes,
confusing popular servers (including but limited to AWS).

This CL partially reverts the no-byte-sniffing behavior and restores
it only for GET/HEAD/DELETE/etc requests, and only when there's no
Transfer-Encoding set, and the Content-Length is 0 or -1.

Updates #18257 (aws-go) bug
And also private bug reports about non-AWS issues.

Updates #18407 also, but haven't yet audited things enough to declare
it fixed.

Change-Id: Ie5284d3e067c181839b31faf637eee56e5738a6a
Reviewed-on: https://go-review.googlesource.com/34668
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
src/net/http/request.go
src/net/http/requestwrite_test.go
src/net/http/transfer.go

index 283595f4626238d3679056c4f5fe6c762dbb4ddd..fb6bb0aab5873a4c6148ff6a22c428d1b9e04439 100644 (file)
@@ -341,6 +341,18 @@ func (r *Request) ProtoAtLeast(major, minor int) bool {
                r.ProtoMajor == major && r.ProtoMinor >= minor
 }
 
+// protoAtLeastOutgoing is like ProtoAtLeast, but is for outgoing
+// requests (see issue 18407) where these fields aren't supposed to
+// matter.  As a minor fix for Go 1.8, at least treat (0, 0) as
+// matching HTTP/1.1 or HTTP/1.0.  Only HTTP/1.1 is used.
+// TODO(bradfitz): ideally remove this whole method. It shouldn't be used.
+func (r *Request) protoAtLeastOutgoing(major, minor int) bool {
+       if r.ProtoMajor == 0 && r.ProtoMinor == 0 && major == 1 && minor <= 1 {
+               return true
+       }
+       return r.ProtoAtLeast(major, minor)
+}
+
 // UserAgent returns the client's User-Agent, if sent in the request.
 func (r *Request) UserAgent() string {
        return r.Header.Get("User-Agent")
@@ -600,6 +612,12 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, wai
                }
        }
 
+       if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
+               if err := bw.Flush(); err != nil {
+                       return err
+               }
+       }
+
        // Write body and trailer
        err = tw.WriteBody(w)
        if err != nil {
@@ -1299,3 +1317,18 @@ func (r *Request) outgoingLength() int64 {
        }
        return -1
 }
+
+// requestMethodUsuallyLacksBody reports whether the given request
+// method is one that typically does not involve a request body.
+// This is used by the Transport (via
+// transferWriter.shouldSendChunkedRequestBody) to determine whether
+// we try to test-read a byte from a non-nil Request.Body when
+// Request.outgoingLength() returns -1. See the comments in
+// shouldSendChunkedRequestBody.
+func requestMethodUsuallyLacksBody(method string) bool {
+       switch method {
+       case "GET", "HEAD", "DELETE", "OPTIONS", "PROPFIND", "SEARCH":
+               return true
+       }
+       return false
+}
index c398e645392e218c51e0fa92309c03966c3394ad..eb65b9f736f5ba81e923f1b33ce837b757e1bc5b 100644 (file)
@@ -5,14 +5,17 @@
 package http
 
 import (
+       "bufio"
        "bytes"
        "errors"
        "fmt"
        "io"
        "io/ioutil"
+       "net"
        "net/url"
        "strings"
        "testing"
+       "time"
 )
 
 type reqWriteTest struct {
@@ -566,6 +569,138 @@ func TestRequestWrite(t *testing.T) {
        }
 }
 
+func TestRequestWriteTransport(t *testing.T) {
+       t.Parallel()
+
+       matchSubstr := func(substr string) func(string) error {
+               return func(written string) error {
+                       if !strings.Contains(written, substr) {
+                               return fmt.Errorf("expected substring %q in request: %s", substr, written)
+                       }
+                       return nil
+               }
+       }
+
+       noContentLengthOrTransferEncoding := func(req string) error {
+               if strings.Contains(req, "Content-Length: ") {
+                       return fmt.Errorf("unexpected Content-Length in request: %s", req)
+               }
+               if strings.Contains(req, "Transfer-Encoding: ") {
+                       return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
+               }
+               return nil
+       }
+
+       all := func(checks ...func(string) error) func(string) error {
+               return func(req string) error {
+                       for _, c := range checks {
+                               if err := c(req); err != nil {
+                                       return err
+                               }
+                       }
+                       return nil
+               }
+       }
+
+       type testCase struct {
+               method string
+               clen   int64 // ContentLength
+               body   io.ReadCloser
+               want   func(string) error
+
+               // optional:
+               init         func(*testCase)
+               afterReqRead func()
+       }
+
+       tests := []testCase{
+               {
+                       method: "GET",
+                       want:   noContentLengthOrTransferEncoding,
+               },
+               {
+                       method: "GET",
+                       body:   ioutil.NopCloser(strings.NewReader("")),
+                       want:   noContentLengthOrTransferEncoding,
+               },
+               {
+                       method: "GET",
+                       clen:   -1,
+                       body:   ioutil.NopCloser(strings.NewReader("")),
+                       want:   noContentLengthOrTransferEncoding,
+               },
+               // A GET with a body, with explicit content length:
+               {
+                       method: "GET",
+                       clen:   7,
+                       body:   ioutil.NopCloser(strings.NewReader("foobody")),
+                       want: all(matchSubstr("Content-Length: 7"),
+                               matchSubstr("foobody")),
+               },
+               // A GET with a body, sniffing the leading "f" from "foobody".
+               {
+                       method: "GET",
+                       clen:   -1,
+                       body:   ioutil.NopCloser(strings.NewReader("foobody")),
+                       want: all(matchSubstr("Transfer-Encoding: chunked"),
+                               matchSubstr("\r\n1\r\nf\r\n"),
+                               matchSubstr("oobody")),
+               },
+               // But a POST request is expected to have a body, so
+               // no sniffing happens:
+               {
+                       method: "POST",
+                       clen:   -1,
+                       body:   ioutil.NopCloser(strings.NewReader("foobody")),
+                       want: all(matchSubstr("Transfer-Encoding: chunked"),
+                               matchSubstr("foobody")),
+               },
+               {
+                       method: "POST",
+                       clen:   -1,
+                       body:   ioutil.NopCloser(strings.NewReader("")),
+                       want:   all(matchSubstr("Transfer-Encoding: chunked")),
+               },
+               // Verify that a blocking Request.Body doesn't block forever.
+               {
+                       method: "GET",
+                       clen:   -1,
+                       init: func(tt *testCase) {
+                               pr, pw := io.Pipe()
+                               tt.afterReqRead = func() {
+                                       pw.Close()
+                               }
+                               tt.body = ioutil.NopCloser(pr)
+                       },
+                       want: matchSubstr("Transfer-Encoding: chunked"),
+               },
+       }
+
+       for i, tt := range tests {
+               if tt.init != nil {
+                       tt.init(&tt)
+               }
+               req := &Request{
+                       Method: tt.method,
+                       URL: &url.URL{
+                               Scheme: "http",
+                               Host:   "example.com",
+                       },
+                       Header:        make(Header),
+                       ContentLength: tt.clen,
+                       Body:          tt.body,
+               }
+               got, err := dumpRequestOut(req, tt.afterReqRead)
+               if err != nil {
+                       t.Errorf("test[%d]: %v", i, err)
+                       continue
+               }
+               if err := tt.want(string(got)); err != nil {
+                       t.Errorf("test[%d]: %v", i, err)
+               }
+       }
+}
+
 type closeChecker struct {
        io.Reader
        closed bool
@@ -672,3 +807,76 @@ func TestRequestWriteError(t *testing.T) {
                t.Fatalf("writeCalls constant is outdated in test")
        }
 }
+
+// dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut.
+// Unlike the original, this version doesn't mutate the req.Body and
+// try to restore it. It always dumps the whole body.
+// And it doesn't support https.
+func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
+
+       // Use the actual Transport code to record what we would send
+       // on the wire, but not using TCP.  Use a Transport with a
+       // custom dialer that returns a fake net.Conn that waits
+       // for the full input (and recording it), and then responds
+       // with a dummy response.
+       var buf bytes.Buffer // records the output
+       pr, pw := io.Pipe()
+       defer pr.Close()
+       defer pw.Close()
+       dr := &delegateReader{c: make(chan io.Reader)}
+
+       t := &Transport{
+               Dial: func(net, addr string) (net.Conn, error) {
+                       return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
+               },
+       }
+       defer t.CloseIdleConnections()
+
+       // Wait for the request before replying with a dummy response:
+       go func() {
+               req, err := ReadRequest(bufio.NewReader(pr))
+               if err == nil {
+                       if onReadHeaders != nil {
+                               onReadHeaders()
+                       }
+                       // Ensure all the body is read; otherwise
+                       // we'll get a partial dump.
+                       io.Copy(ioutil.Discard, req.Body)
+                       req.Body.Close()
+               }
+               dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
+       }()
+
+       _, err := t.RoundTrip(req)
+       if err != nil {
+               return nil, err
+       }
+       return buf.Bytes(), nil
+}
+
+// delegateReader is a reader that delegates to another reader,
+// once it arrives on a channel.
+type delegateReader struct {
+       c chan io.Reader
+       r io.Reader // nil until received from c
+}
+
+func (r *delegateReader) Read(p []byte) (int, error) {
+       if r.r == nil {
+               r.r = <-r.c
+       }
+       return r.r.Read(p)
+}
+
+// dumpConn is a net.Conn that writes to Writer and reads from Reader.
+type dumpConn struct {
+       io.Writer
+       io.Reader
+}
+
+func (c *dumpConn) Close() error                       { return nil }
+func (c *dumpConn) LocalAddr() net.Addr                { return nil }
+func (c *dumpConn) RemoteAddr() net.Addr               { return nil }
+func (c *dumpConn) SetDeadline(t time.Time) error      { return nil }
+func (c *dumpConn) SetReadDeadline(t time.Time) error  { return nil }
+func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }
index beafb7ac97f4014d74394bc7770e41d97e469b59..4f47637aa76a6af7005a885f020cfec5484320ae 100644 (file)
@@ -17,6 +17,7 @@ import (
        "strconv"
        "strings"
        "sync"
+       "time"
 
        "golang_org/x/net/lex/httplex"
 )
@@ -33,6 +34,23 @@ func (r errorReader) Read(p []byte) (n int, err error) {
        return 0, r.err
 }
 
+type byteReader struct {
+       b    byte
+       done bool
+}
+
+func (br *byteReader) Read(p []byte) (n int, err error) {
+       if br.done {
+               return 0, io.EOF
+       }
+       if len(p) == 0 {
+               return 0, nil
+       }
+       br.done = true
+       p[0] = br.b
+       return 1, io.EOF
+}
+
 // transferWriter inspects the fields of a user-supplied Request or Response,
 // sanitizes them without changing the user object and provides methods for
 // writing the respective header, body and trailer in wire format.
@@ -46,6 +64,9 @@ type transferWriter struct {
        TransferEncoding []string
        Trailer          Header
        IsResponse       bool
+
+       FlushHeaders bool            // flush headers to network before body
+       ByteReadCh   chan readResult // non-nil if probeRequestBody called
 }
 
 func newTransferWriter(r interface{}) (t *transferWriter, err error) {
@@ -62,14 +83,11 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) {
                t.Close = rr.Close
                t.TransferEncoding = rr.TransferEncoding
                t.Trailer = rr.Trailer
-               atLeastHTTP11 = rr.ProtoAtLeast(1, 1)
-
+               atLeastHTTP11 = rr.protoAtLeastOutgoing(1, 1)
                t.Body = rr.Body
+               t.BodyCloser = rr.Body
                t.ContentLength = rr.outgoingLength()
-               if t.Body != nil {
-                       t.BodyCloser = rr.Body
-               }
-               if t.ContentLength < 0 && len(t.TransferEncoding) == 0 && atLeastHTTP11 {
+               if t.ContentLength < 0 && len(t.TransferEncoding) == 0 && atLeastHTTP11 && t.shouldSendChunkedRequestBody() {
                        t.TransferEncoding = []string{"chunked"}
                }
        case *Response:
@@ -84,7 +102,7 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) {
                t.TransferEncoding = rr.TransferEncoding
                t.Trailer = rr.Trailer
                atLeastHTTP11 = rr.ProtoAtLeast(1, 1)
-               t.ResponseToHEAD = noBodyExpected(t.Method)
+               t.ResponseToHEAD = noResponseBodyExpected(t.Method)
        }
 
        // Sanitize Body,ContentLength,TransferEncoding
@@ -112,7 +130,100 @@ func newTransferWriter(r interface{}) (t *transferWriter, err error) {
        return t, nil
 }
 
-func noBodyExpected(requestMethod string) bool {
+// shouldSendChunkedRequestBody reports whether we should try to send a
+// chunked request body to the server. In particular, the case we really
+// want to prevent is sending a GET or other typically-bodyless request to a
+// server with a chunked body when the body has zero bytes, since GETs with
+// bodies (while acceptable according to specs), even zero-byte chunked
+// bodies, are approximately never seen in the wild and confuse most
+// servers. See Issue 18257, as one example.
+//
+// The only reason we'd send such a request is if the user set the Body to a
+// non-nil value (say, ioutil.NopCloser(bytes.NewReader(nil))) and didn't
+// set ContentLength, or NewRequest set it to -1 (unknown), so then we assume
+// there's bytes to send.
+//
+// This code tries to read a byte from the Request.Body in such cases to see
+// whether the body actually has content (super rare) or is actually just
+// a non-nil content-less ReadCloser (the more common case). In that more
+// common case, we act as if their Body were nil instead, and don't send
+// a body.
+func (t *transferWriter) shouldSendChunkedRequestBody() bool {
+       // Note that t.ContentLength is the corrected content length
+       // from rr.outgoingLength, so 0 actually means zero, not unknown.
+       if t.ContentLength >= 0 || t.Body == nil { // redundant checks; caller did them
+               return false
+       }
+       if requestMethodUsuallyLacksBody(t.Method) {
+               // Only probe the Request.Body for GET/HEAD/DELETE/etc
+               // requests, because it's only those types of requests
+               // that confuse servers.
+               t.probeRequestBody() // adjusts t.Body, t.ContentLength
+               return t.Body != nil
+       }
+       // For all other request types (PUT, POST, PATCH, or anything
+       // made-up we've never heard of), assume it's normal and the server
+       // can deal with a chunked request body. Maybe we'll adjust this
+       // later.
+       return true
+}
+
+// probeRequestBody reads a byte from t.Body to see whether it's empty
+// (returns io.EOF right away).
+//
+// But because we've had problems with this blocking users in the past
+// (issue 17480) when the body is a pipe (perhaps waiting on the response
+// headers before the pipe is fed data), we need to be careful and bound how
+// long we wait for it. This delay will only affect users if all the following
+// are true:
+//   * the request body blocks
+//   * the content length is not set (or set to -1)
+//   * the method doesn't usually have a body (GET, HEAD, DELETE, ...)
+//   * there is no transfer-encoding=chunked already set.
+// In other words, this delay will not normally affect anybody, and there
+// are workarounds if it does.
+func (t *transferWriter) probeRequestBody() {
+       t.ByteReadCh = make(chan readResult, 1)
+       go func(body io.Reader) {
+               var buf [1]byte
+               var rres readResult
+               rres.n, rres.err = body.Read(buf[:])
+               if rres.n == 1 {
+                       rres.b = buf[0]
+               }
+               t.ByteReadCh <- rres
+       }(t.Body)
+       timer := time.NewTimer(200 * time.Millisecond)
+       select {
+       case rres := <-t.ByteReadCh:
+               timer.Stop()
+               if rres.n == 0 && rres.err == io.EOF {
+                       // It was empty.
+                       t.Body = nil
+                       t.ContentLength = 0
+               } else if rres.n == 1 {
+                       if rres.err != nil {
+                               t.Body = io.MultiReader(&byteReader{b: rres.b}, errorReader{rres.err})
+                       } else {
+                               t.Body = io.MultiReader(&byteReader{b: rres.b}, t.Body)
+                       }
+               } else if rres.err != nil {
+                       t.Body = errorReader{rres.err}
+               }
+       case <-timer.C:
+               // Too slow. Don't wait. Read it later, and keep
+               // assuming that this is ContentLength == -1
+               // (unknown), which means we'll send a
+               // "Transfer-Encoding: chunked" header.
+               t.Body = io.MultiReader(finishAsyncByteRead{t}, t.Body)
+               // Request that Request.Write flush the headers to the
+               // network before writing the body, since our body may not
+               // become readable until it's seen the response headers.
+               t.FlushHeaders = true
+       }
+}
+
+func noResponseBodyExpected(requestMethod string) bool {
        return requestMethod == "HEAD"
 }
 
@@ -216,7 +327,9 @@ func (t *transferWriter) WriteBody(w io.Writer) error {
                if err != nil {
                        return err
                }
-               if err = t.BodyCloser.Close(); err != nil {
+       }
+       if t.BodyCloser != nil {
+               if err := t.BodyCloser.Close(); err != nil {
                        return err
                }
        }
@@ -366,7 +479,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
        // or close connection when finished, since multipart is not supported yet
        switch {
        case chunked(t.TransferEncoding):
-               if noBodyExpected(t.RequestMethod) {
+               if noResponseBodyExpected(t.RequestMethod) {
                        t.Body = NoBody
                } else {
                        t.Body = &body{src: internal.NewChunkedReader(r), hdr: msg, r: r, closing: t.Close}
@@ -498,7 +611,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,
        }
 
        // Logic based on response type or status
-       if noBodyExpected(requestMethod) {
+       if noResponseBodyExpected(requestMethod) {
                // For HTTP requests, as part of hardening against request
                // smuggling (RFC 7230), don't allow a Content-Length header for
                // methods which don't permit bodies. As an exception, allow
@@ -861,3 +974,21 @@ func parseContentLength(cl string) (int64, error) {
        return n, nil
 
 }
+
+// finishAsyncByteRead finishes reading the 1-byte sniff
+// from the ContentLength==0, Body!=nil case.
+type finishAsyncByteRead struct {
+       tw *transferWriter
+}
+
+func (fr finishAsyncByteRead) Read(p []byte) (n int, err error) {
+       if len(p) == 0 {
+               return
+       }
+       rres := <-fr.tw.ByteReadCh
+       n, err = rres.n, rres.err
+       if n == 1 {
+               p[0] = rres.b
+       }
+       return
+}