Commit 6e36811c authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: restore Transport's Request.Body byte sniff in limited cases

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: 's avatarIan Lance Taylor <iant@golang.org>
parent f419b563
......@@ -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
}
......@@ -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 }
......@@ -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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment