blob: a8ce2d3709ab3d6ab37635a30b3e4faf156aabc8 [file] [log] [blame]
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package http
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
)
func TestBodyReadBadTrailer(t *testing.T) {
b := &body{
src: strings.NewReader("foobar"),
hdr: true, // force reading the trailer
r: bufio.NewReader(strings.NewReader("")),
}
buf := make([]byte, 7)
n, err := b.Read(buf[:3])
got := string(buf[:n])
if got != "foo" || err != nil {
t.Fatalf(`first Read = %d (%q), %v; want 3 ("foo")`, n, got, err)
}
n, err = b.Read(buf[:])
got = string(buf[:n])
if got != "bar" || err != nil {
t.Fatalf(`second Read = %d (%q), %v; want 3 ("bar")`, n, got, err)
}
n, err = b.Read(buf[:])
got = string(buf[:n])
if err == nil {
t.Errorf("final Read was successful (%q), expected error from trailer read", got)
}
}
func TestFinalChunkedBodyReadEOF(t *testing.T) {
res, err := ReadResponse(bufio.NewReader(strings.NewReader(
"HTTP/1.1 200 OK\r\n"+
"Transfer-Encoding: chunked\r\n"+
"\r\n"+
"0a\r\n"+
"Body here\n\r\n"+
"09\r\n"+
"continued\r\n"+
"0\r\n"+
"\r\n")), nil)
if err != nil {
t.Fatal(err)
}
want := "Body here\ncontinued"
buf := make([]byte, len(want))
n, err := res.Body.Read(buf)
if n != len(want) || err != io.EOF {
t.Errorf("Read = %v, %v; want %d, EOF", n, err, len(want))
}
if string(buf) != want {
t.Errorf("buf = %q; want %q", buf, want)
}
}
func TestDetectInMemoryReaders(t *testing.T) {
pr, _ := io.Pipe()
tests := []struct {
r io.Reader
want bool
}{
{pr, false},
{bytes.NewReader(nil), true},
{bytes.NewBuffer(nil), true},
{strings.NewReader(""), true},
{ioutil.NopCloser(pr), false},
{ioutil.NopCloser(bytes.NewReader(nil)), true},
{ioutil.NopCloser(bytes.NewBuffer(nil)), true},
{ioutil.NopCloser(strings.NewReader("")), true},
}
for i, tt := range tests {
got := isKnownInMemoryReader(tt.r)
if got != tt.want {
t.Errorf("%d: got = %v; want %v", i, got, tt.want)
}
}
}
type mockTransferWriter struct {
CalledReader io.Reader
WriteCalled bool
}
var _ io.ReaderFrom = (*mockTransferWriter)(nil)
func (w *mockTransferWriter) ReadFrom(r io.Reader) (int64, error) {
w.CalledReader = r
return io.Copy(ioutil.Discard, r)
}
func (w *mockTransferWriter) Write(p []byte) (int, error) {
w.WriteCalled = true
return ioutil.Discard.Write(p)
}
func TestTransferWriterWriteBodyReaderTypes(t *testing.T) {
fileType := reflect.TypeOf(&os.File{})
bufferType := reflect.TypeOf(&bytes.Buffer{})
nBytes := int64(1 << 10)
newFileFunc := func() (r io.Reader, done func(), err error) {
f, err := ioutil.TempFile("", "net-http-newfilefunc")
if err != nil {
return nil, nil, err
}
// Write some bytes to the file to enable reading.
if _, err := io.CopyN(f, rand.Reader, nBytes); err != nil {
return nil, nil, fmt.Errorf("failed to write data to file: %v", err)
}
if _, err := f.Seek(0, 0); err != nil {
return nil, nil, fmt.Errorf("failed to seek to front: %v", err)
}
done = func() {
f.Close()
os.Remove(f.Name())
}
return f, done, nil
}
newBufferFunc := func() (io.Reader, func(), error) {
return bytes.NewBuffer(make([]byte, nBytes)), func() {}, nil
}
cases := []struct {
name string
bodyFunc func() (io.Reader, func(), error)
method string
contentLength int64
transferEncoding []string
limitedReader bool
expectedReader reflect.Type
expectedWrite bool
}{
{
name: "file, non-chunked, size set",
bodyFunc: newFileFunc,
method: "PUT",
contentLength: nBytes,
limitedReader: true,
expectedReader: fileType,
},
{
name: "file, non-chunked, size set, nopCloser wrapped",
method: "PUT",
bodyFunc: func() (io.Reader, func(), error) {
r, cleanup, err := newFileFunc()
return ioutil.NopCloser(r), cleanup, err
},
contentLength: nBytes,
limitedReader: true,
expectedReader: fileType,
},
{
name: "file, non-chunked, negative size",
method: "PUT",
bodyFunc: newFileFunc,
contentLength: -1,
expectedReader: fileType,
},
{
name: "file, non-chunked, CONNECT, negative size",
method: "CONNECT",
bodyFunc: newFileFunc,
contentLength: -1,
expectedReader: fileType,
},
{
name: "file, chunked",
method: "PUT",
bodyFunc: newFileFunc,
transferEncoding: []string{"chunked"},
expectedWrite: true,
},
{
name: "buffer, non-chunked, size set",
bodyFunc: newBufferFunc,
method: "PUT",
contentLength: nBytes,
limitedReader: true,
expectedReader: bufferType,
},
{
name: "buffer, non-chunked, size set, nopCloser wrapped",
method: "PUT",
bodyFunc: func() (io.Reader, func(), error) {
r, cleanup, err := newBufferFunc()
return ioutil.NopCloser(r), cleanup, err
},
contentLength: nBytes,
limitedReader: true,
expectedReader: bufferType,
},
{
name: "buffer, non-chunked, negative size",
method: "PUT",
bodyFunc: newBufferFunc,
contentLength: -1,
expectedWrite: true,
},
{
name: "buffer, non-chunked, CONNECT, negative size",
method: "CONNECT",
bodyFunc: newBufferFunc,
contentLength: -1,
expectedWrite: true,
},
{
name: "buffer, chunked",
method: "PUT",
bodyFunc: newBufferFunc,
transferEncoding: []string{"chunked"},
expectedWrite: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
body, cleanup, err := tc.bodyFunc()
if err != nil {
t.Fatal(err)
}
defer cleanup()
mw := &mockTransferWriter{}
tw := &transferWriter{
Body: body,
ContentLength: tc.contentLength,
TransferEncoding: tc.transferEncoding,
}
if err := tw.writeBody(mw); err != nil {
t.Fatal(err)
}
if tc.expectedReader != nil {
if mw.CalledReader == nil {
t.Fatal("did not call ReadFrom")
}
var actualReader reflect.Type
lr, ok := mw.CalledReader.(*io.LimitedReader)
if ok && tc.limitedReader {
actualReader = reflect.TypeOf(lr.R)
} else {
actualReader = reflect.TypeOf(mw.CalledReader)
}
if tc.expectedReader != actualReader {
t.Fatalf("got reader %T want %T", actualReader, tc.expectedReader)
}
}
if tc.expectedWrite && !mw.WriteCalled {
t.Fatal("did not invoke Write")
}
})
}
}
func TestFixTransferEncoding(t *testing.T) {
tests := []struct {
hdr Header
wantErr error
}{
{
hdr: Header{"Transfer-Encoding": {"fugazi"}},
wantErr: &unsupportedTEError{`unsupported transfer encoding: "fugazi"`},
},
{
hdr: Header{"Transfer-Encoding": {"chunked, chunked", "identity", "chunked"}},
wantErr: &badStringError{"chunked must be applied only once, as the last encoding", "chunked, chunked"},
},
{
hdr: Header{"Transfer-Encoding": {"chunked"}},
wantErr: nil,
},
}
for i, tt := range tests {
tr := &transferReader{
Header: tt.hdr,
ProtoMajor: 1,
ProtoMinor: 1,
}
gotErr := tr.fixTransferEncoding()
if !reflect.DeepEqual(gotErr, tt.wantErr) {
t.Errorf("%d.\ngot error:\n%v\nwant error:\n%v\n\n", i, gotErr, tt.wantErr)
}
}
}
func gzipIt(s string) string {
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
gw.Write([]byte(s))
gw.Close()
return buf.String()
}
func TestUnitTestProxyingReadCloserClosesBody(t *testing.T) {
var checker closeChecker
buf := new(bytes.Buffer)
buf.WriteString("Hello, Gophers!")
prc := &proxyingReadCloser{
Reader: buf,
Closer: &checker,
}
prc.Close()
read, err := ioutil.ReadAll(prc)
if err != nil {
t.Fatalf("Read error: %v", err)
}
if g, w := string(read), "Hello, Gophers!"; g != w {
t.Errorf("Read mismatch: got %q want %q", g, w)
}
if checker.closed != true {
t.Fatal("closeChecker.Close was never invoked")
}
}
func TestGzipTransferEncoding_request(t *testing.T) {
helloWorldGzipped := gzipIt("Hello, World!")
tests := []struct {
payload string
wantErr string
wantBody string
}{
{
// The case of "chunked" properly applied as the last encoding
// and a gzipped request payload that is streamed in 3 parts.
payload: `POST / HTTP/1.1
Host: golang.org
Transfer-Encoding: gzip, chunked
Content-Type: text/html; charset=UTF-8
` + fmt.Sprintf("%02x\r\n%s\r\n%02x\r\n%s\r\n%02x\r\n%s\r\n0\r\n\r\n",
3, helloWorldGzipped[:3],
5, helloWorldGzipped[3:8],
len(helloWorldGzipped)-8, helloWorldGzipped[8:]),
wantBody: `Hello, World!`,
},
{
// The request specifies "Transfer-Encoding: chunked" so its body must be left untouched.
payload: `PUT / HTTP/1.1
Host: golang.org
Transfer-Encoding: chunked
Connection: close
Content-Type: text/html; charset=UTF-8
` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped),
// We want that payload as it was sent.
wantBody: helloWorldGzipped,
},
{
// Valid request, the body doesn't have "Transfer-Encoding: chunked" but implicitly encoded
// for chunking as per the advisory from RFC 7230 3.3.1 which advises for cases where.
payload: `POST / HTTP/1.1
Host: localhost
Transfer-Encoding: gzip
Content-Type: text/html; charset=UTF-8
` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped),
wantBody: `Hello, World!`,
},
{
// Invalid request, the body isn't chunked nor is the connection terminated immediately
// hence invalid as per the advisory from RFC 7230 3.3.1 which advises for cases where
// a Transfer-Encoding that isn't finally chunked is provided.
payload: `PUT / HTTP/1.1
Host: golang.org
Transfer-Encoding: gzip
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
`,
wantErr: `EOF`,
},
{
// The case of chunked applied before another encoding.
payload: `PUT / HTTP/1.1
Location: golang.org
Transfer-Encoding: chunked, gzip
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
`,
wantErr: `chunked must be applied only once, as the last encoding "chunked, gzip"`,
},
{
// The case of chunked properly applied as the
// last encoding BUT with a bad "Content-Length".
payload: `POST / HTTP/1.1
Host: golang.org
Transfer-Encoding: gzip, chunked
Content-Length: 10
Connection: close
Content-Type: text/html; charset=UTF-8
` + "0\r\n\r\n",
wantErr: "EOF",
},
}
for i, tt := range tests {
req, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.payload)))
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("test %d. Error mismatch\nGot: %v\nWant: %s", i, err, tt.wantErr)
}
continue
}
if err != nil {
t.Errorf("test %d. Unexpected ReadRequest error: %v\nPayload:\n%s", i, err, tt.payload)
continue
}
got, err := ioutil.ReadAll(req.Body)
req.Body.Close()
if err != nil {
t.Errorf("test %d. Failed to read response body: %v", i, err)
}
if g, w := string(got), tt.wantBody; g != w {
t.Errorf("test %d. Request body mimsatch\nGot:\n%s\n\nWant:\n%s", i, g, w)
}
}
}
func TestGzipTransferEncoding_response(t *testing.T) {
helloWorldGzipped := gzipIt("Hello, World!")
tests := []struct {
payload string
wantErr string
wantBody string
}{
{
// The case of "chunked" properly applied as the last encoding
// and a gzipped payload that is streamed in 3 parts.
payload: `HTTP/1.1 302 Found
Location: https://golang.org/
Transfer-Encoding: gzip, chunked
Connection: close
Content-Type: text/html; charset=UTF-8
` + fmt.Sprintf("%02x\r\n%s\r\n%02x\r\n%s\r\n%02x\r\n%s\r\n0\r\n\r\n",
3, helloWorldGzipped[:3],
5, helloWorldGzipped[3:8],
len(helloWorldGzipped)-8, helloWorldGzipped[8:]),
wantBody: `Hello, World!`,
},
{
// The response specifies "Transfer-Encoding: chunked" so response body must be left untouched.
payload: `HTTP/1.1 302 Found
Location: https://golang.org/
Transfer-Encoding: chunked
Connection: close
Content-Type: text/html; charset=UTF-8
` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped),
// We want that payload as it was sent.
wantBody: helloWorldGzipped,
},
{
// Valid response, the body doesn't have "Transfer-Encoding: chunked" but implicitly encoded
// for chunking as per the advisory from RFC 7230 3.3.1 which advises for cases where.
payload: `HTTP/1.1 302 Found
Location: https://golang.org/
Transfer-Encoding: gzip
Connection: close
Content-Type: text/html; charset=UTF-8
` + fmt.Sprintf("%0x\r\n%s\r\n0\r\n\r\n", len(helloWorldGzipped), helloWorldGzipped),
wantBody: `Hello, World!`,
},
{
// Invalid response, the body isn't chunked nor is the connection terminated immediately
// hence invalid as per the advisory from RFC 7230 3.3.1 which advises for cases where
// a Transfer-Encoding that isn't finally chunked is provided.
payload: `HTTP/1.1 302 Found
Location: https://golang.org/
Transfer-Encoding: gzip
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
`,
wantErr: `EOF`,
},
{
// The case of chunked applied before another encoding.
payload: `HTTP/1.1 302 Found
Location: https://golang.org/
Transfer-Encoding: chunked, gzip
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
`,
wantErr: `chunked must be applied only once, as the last encoding "chunked, gzip"`,
},
{
// The case of chunked properly applied as the
// last encoding BUT with a bad "Content-Length".
payload: `HTTP/1.1 302 Found
Location: https://golang.org/
Transfer-Encoding: gzip, chunked
Content-Length: 10
Connection: close
Content-Type: text/html; charset=UTF-8
` + "0\r\n\r\n",
wantErr: "EOF",
},
{
// Including "identity" more than once.
payload: `HTTP/1.1 200 OK
Location: https://golang.org/
Transfer-Encoding: identity, identity
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8
` + "0\r\n\r\n",
wantErr: `"identity" when present must be the only transfer encoding "identity, identity"`,
},
}
for i, tt := range tests {
res, err := ReadResponse(bufio.NewReader(strings.NewReader(tt.payload)), nil)
if tt.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("test %d. Error mismatch\nGot: %v\nWant: %s", i, err, tt.wantErr)
}
continue
}
if err != nil {
t.Errorf("test %d. Unexpected ReadResponse error: %v\nPayload:\n%s", i, err, tt.payload)
continue
}
got, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("test %d. Failed to read response body: %v", i, err)
}
if g, w := string(got), tt.wantBody; g != w {
t.Errorf("test %d. Response body mimsatch\nGot:\n%s\n\nWant:\n%s", i, g, w)
}
}
}