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