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 5package httputil 6 7import ( 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 20type 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 29var 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 200func 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 253func chunk(s string) string { 254 return fmt.Sprintf("%x\r\n%s\r\n", len(s), s) 255} 256 257func 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 265func 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 273func 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 281var 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 301Content-Length: 50 302Foo: 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 317Content-Length: 3 318 319foo`, 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 335Transfer-Encoding: chunked 336 3373 338foo 3390`, 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 358Foo1: Bar1 359Foo2: Bar2 360Content-Length: 0`, 361 }, 362} 363 364func 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.ReplaceAll(got, "\r", "") 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