Home | History | Annotate | Download | only in httputil
      1 // Copyright 2011 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package httputil
      6 
      7 import (
      8 	"bufio"
      9 	"bytes"
     10 	"fmt"
     11 	"io"
     12 	"io/ioutil"
     13 	"net/http"
     14 	"net/url"
     15 	"runtime"
     16 	"strings"
     17 	"testing"
     18 )
     19 
     20 type dumpTest struct {
     21 	Req  http.Request
     22 	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
     23 
     24 	WantDump    string
     25 	WantDumpOut string
     26 	NoBody      bool // if true, set DumpRequest{,Out} body to false
     27 }
     28 
     29 var dumpTests = []dumpTest{
     30 	// HTTP/1.1 => chunked coding; body; empty trailer
     31 	{
     32 		Req: http.Request{
     33 			Method: "GET",
     34 			URL: &url.URL{
     35 				Scheme: "http",
     36 				Host:   "www.google.com",
     37 				Path:   "/search",
     38 			},
     39 			ProtoMajor:       1,
     40 			ProtoMinor:       1,
     41 			TransferEncoding: []string{"chunked"},
     42 		},
     43 
     44 		Body: []byte("abcdef"),
     45 
     46 		WantDump: "GET /search HTTP/1.1\r\n" +
     47 			"Host: www.google.com\r\n" +
     48 			"Transfer-Encoding: chunked\r\n\r\n" +
     49 			chunk("abcdef") + chunk(""),
     50 	},
     51 
     52 	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
     53 	// and doesn't add a User-Agent.
     54 	{
     55 		Req: http.Request{
     56 			Method:     "GET",
     57 			URL:        mustParseURL("/foo"),
     58 			ProtoMajor: 1,
     59 			ProtoMinor: 0,
     60 			Header: http.Header{
     61 				"X-Foo": []string{"X-Bar"},
     62 			},
     63 		},
     64 
     65 		WantDump: "GET /foo HTTP/1.0\r\n" +
     66 			"X-Foo: X-Bar\r\n\r\n",
     67 	},
     68 
     69 	{
     70 		Req: *mustNewRequest("GET", "http://example.com/foo", nil),
     71 
     72 		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
     73 			"Host: example.com\r\n" +
     74 			"User-Agent: Go-http-client/1.1\r\n" +
     75 			"Accept-Encoding: gzip\r\n\r\n",
     76 	},
     77 
     78 	// Test that an https URL doesn't try to do an SSL negotiation
     79 	// with a bytes.Buffer and hang with all goroutines not
     80 	// runnable.
     81 	{
     82 		Req: *mustNewRequest("GET", "https://example.com/foo", nil),
     83 
     84 		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
     85 			"Host: example.com\r\n" +
     86 			"User-Agent: Go-http-client/1.1\r\n" +
     87 			"Accept-Encoding: gzip\r\n\r\n",
     88 	},
     89 
     90 	// Request with Body, but Dump requested without it.
     91 	{
     92 		Req: http.Request{
     93 			Method: "POST",
     94 			URL: &url.URL{
     95 				Scheme: "http",
     96 				Host:   "post.tld",
     97 				Path:   "/",
     98 			},
     99 			ContentLength: 6,
    100 			ProtoMajor:    1,
    101 			ProtoMinor:    1,
    102 		},
    103 
    104 		Body: []byte("abcdef"),
    105 
    106 		WantDumpOut: "POST / HTTP/1.1\r\n" +
    107 			"Host: post.tld\r\n" +
    108 			"User-Agent: Go-http-client/1.1\r\n" +
    109 			"Content-Length: 6\r\n" +
    110 			"Accept-Encoding: gzip\r\n\r\n",
    111 
    112 		NoBody: true,
    113 	},
    114 
    115 	// Request with Body > 8196 (default buffer size)
    116 	{
    117 		Req: http.Request{
    118 			Method: "POST",
    119 			URL: &url.URL{
    120 				Scheme: "http",
    121 				Host:   "post.tld",
    122 				Path:   "/",
    123 			},
    124 			Header: http.Header{
    125 				"Content-Length": []string{"8193"},
    126 			},
    127 
    128 			ContentLength: 8193,
    129 			ProtoMajor:    1,
    130 			ProtoMinor:    1,
    131 		},
    132 
    133 		Body: bytes.Repeat([]byte("a"), 8193),
    134 
    135 		WantDumpOut: "POST / HTTP/1.1\r\n" +
    136 			"Host: post.tld\r\n" +
    137 			"User-Agent: Go-http-client/1.1\r\n" +
    138 			"Content-Length: 8193\r\n" +
    139 			"Accept-Encoding: gzip\r\n\r\n" +
    140 			strings.Repeat("a", 8193),
    141 		WantDump: "POST / HTTP/1.1\r\n" +
    142 			"Host: post.tld\r\n" +
    143 			"Content-Length: 8193\r\n\r\n" +
    144 			strings.Repeat("a", 8193),
    145 	},
    146 
    147 	{
    148 		Req: *mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
    149 			"User-Agent: blah\r\n\r\n"),
    150 		NoBody: true,
    151 		WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
    152 			"User-Agent: blah\r\n\r\n",
    153 	},
    154 
    155 	// Issue #7215. DumpRequest should return the "Content-Length" when set
    156 	{
    157 		Req: *mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
    158 			"Host: passport.myhost.com\r\n" +
    159 			"Content-Length: 3\r\n" +
    160 			"\r\nkey1=name1&key2=name2"),
    161 		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
    162 			"Host: passport.myhost.com\r\n" +
    163 			"Content-Length: 3\r\n" +
    164 			"\r\nkey",
    165 	},
    166 
    167 	// Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
    168 	{
    169 		Req: *mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
    170 			"Host: passport.myhost.com\r\n" +
    171 			"Content-Length: 0\r\n" +
    172 			"\r\nkey1=name1&key2=name2"),
    173 		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
    174 			"Host: passport.myhost.com\r\n" +
    175 			"Content-Length: 0\r\n\r\n",
    176 	},
    177 
    178 	// Issue #7215. DumpRequest should not return the "Content-Length" if unset
    179 	{
    180 		Req: *mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
    181 			"Host: passport.myhost.com\r\n" +
    182 			"\r\nkey1=name1&key2=name2"),
    183 		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
    184 			"Host: passport.myhost.com\r\n\r\n",
    185 	},
    186 
    187 	// Issue 18506: make drainBody recognize NoBody. Otherwise
    188 	// this was turning into a chunked request.
    189 	{
    190 		Req: *mustNewRequest("POST", "http://example.com/foo", http.NoBody),
    191 
    192 		WantDumpOut: "POST /foo HTTP/1.1\r\n" +
    193 			"Host: example.com\r\n" +
    194 			"User-Agent: Go-http-client/1.1\r\n" +
    195 			"Content-Length: 0\r\n" +
    196 			"Accept-Encoding: gzip\r\n\r\n",
    197 	},
    198 }
    199 
    200 func TestDumpRequest(t *testing.T) {
    201 	numg0 := runtime.NumGoroutine()
    202 	for i, tt := range dumpTests {
    203 		setBody := func() {
    204 			if tt.Body == nil {
    205 				return
    206 			}
    207 			switch b := tt.Body.(type) {
    208 			case []byte:
    209 				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
    210 			case func() io.ReadCloser:
    211 				tt.Req.Body = b()
    212 			default:
    213 				t.Fatalf("Test %d: unsupported Body of %T", i, tt.Body)
    214 			}
    215 		}
    216 		if tt.Req.Header == nil {
    217 			tt.Req.Header = make(http.Header)
    218 		}
    219 
    220 		if tt.WantDump != "" {
    221 			setBody()
    222 			dump, err := DumpRequest(&tt.Req, !tt.NoBody)
    223 			if err != nil {
    224 				t.Errorf("DumpRequest #%d: %s", i, err)
    225 				continue
    226 			}
    227 			if string(dump) != tt.WantDump {
    228 				t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
    229 				continue
    230 			}
    231 		}
    232 
    233 		if tt.WantDumpOut != "" {
    234 			setBody()
    235 			dump, err := DumpRequestOut(&tt.Req, !tt.NoBody)
    236 			if err != nil {
    237 				t.Errorf("DumpRequestOut #%d: %s", i, err)
    238 				continue
    239 			}
    240 			if string(dump) != tt.WantDumpOut {
    241 				t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
    242 				continue
    243 			}
    244 		}
    245 	}
    246 	if dg := runtime.NumGoroutine() - numg0; dg > 4 {
    247 		buf := make([]byte, 4096)
    248 		buf = buf[:runtime.Stack(buf, true)]
    249 		t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
    250 	}
    251 }
    252 
    253 func chunk(s string) string {
    254 	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
    255 }
    256 
    257 func mustParseURL(s string) *url.URL {
    258 	u, err := url.Parse(s)
    259 	if err != nil {
    260 		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
    261 	}
    262 	return u
    263 }
    264 
    265 func mustNewRequest(method, url string, body io.Reader) *http.Request {
    266 	req, err := http.NewRequest(method, url, body)
    267 	if err != nil {
    268 		panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
    269 	}
    270 	return req
    271 }
    272 
    273 func mustReadRequest(s string) *http.Request {
    274 	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
    275 	if err != nil {
    276 		panic(err)
    277 	}
    278 	return req
    279 }
    280 
    281 var dumpResTests = []struct {
    282 	res  *http.Response
    283 	body bool
    284 	want string
    285 }{
    286 	{
    287 		res: &http.Response{
    288 			Status:        "200 OK",
    289 			StatusCode:    200,
    290 			Proto:         "HTTP/1.1",
    291 			ProtoMajor:    1,
    292 			ProtoMinor:    1,
    293 			ContentLength: 50,
    294 			Header: http.Header{
    295 				"Foo": []string{"Bar"},
    296 			},
    297 			Body: ioutil.NopCloser(strings.NewReader("foo")), // shouldn't be used
    298 		},
    299 		body: false, // to verify we see 50, not empty or 3.
    300 		want: `HTTP/1.1 200 OK
    301 Content-Length: 50
    302 Foo: Bar`,
    303 	},
    304 
    305 	{
    306 		res: &http.Response{
    307 			Status:        "200 OK",
    308 			StatusCode:    200,
    309 			Proto:         "HTTP/1.1",
    310 			ProtoMajor:    1,
    311 			ProtoMinor:    1,
    312 			ContentLength: 3,
    313 			Body:          ioutil.NopCloser(strings.NewReader("foo")),
    314 		},
    315 		body: true,
    316 		want: `HTTP/1.1 200 OK
    317 Content-Length: 3
    318 
    319 foo`,
    320 	},
    321 
    322 	{
    323 		res: &http.Response{
    324 			Status:           "200 OK",
    325 			StatusCode:       200,
    326 			Proto:            "HTTP/1.1",
    327 			ProtoMajor:       1,
    328 			ProtoMinor:       1,
    329 			ContentLength:    -1,
    330 			Body:             ioutil.NopCloser(strings.NewReader("foo")),
    331 			TransferEncoding: []string{"chunked"},
    332 		},
    333 		body: true,
    334 		want: `HTTP/1.1 200 OK
    335 Transfer-Encoding: chunked
    336 
    337 3
    338 foo
    339 0`,
    340 	},
    341 	{
    342 		res: &http.Response{
    343 			Status:        "200 OK",
    344 			StatusCode:    200,
    345 			Proto:         "HTTP/1.1",
    346 			ProtoMajor:    1,
    347 			ProtoMinor:    1,
    348 			ContentLength: 0,
    349 			Header: http.Header{
    350 				// To verify if headers are not filtered out.
    351 				"Foo1": []string{"Bar1"},
    352 				"Foo2": []string{"Bar2"},
    353 			},
    354 			Body: nil,
    355 		},
    356 		body: false, // to verify we see 0, not empty.
    357 		want: `HTTP/1.1 200 OK
    358 Foo1: Bar1
    359 Foo2: Bar2
    360 Content-Length: 0`,
    361 	},
    362 }
    363 
    364 func TestDumpResponse(t *testing.T) {
    365 	for i, tt := range dumpResTests {
    366 		gotb, err := DumpResponse(tt.res, tt.body)
    367 		if err != nil {
    368 			t.Errorf("%d. DumpResponse = %v", i, err)
    369 			continue
    370 		}
    371 		got := string(gotb)
    372 		got = strings.TrimSpace(got)
    373 		got = strings.Replace(got, "\r", "", -1)
    374 
    375 		if got != tt.want {
    376 			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
    377 		}
    378 	}
    379 }
    380