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	"context"
11	"fmt"
12	"io"
13	"math/rand"
14	"net/http"
15	"net/url"
16	"runtime"
17	"runtime/pprof"
18	"strings"
19	"testing"
20	"time"
21)
22
23type eofReader struct{}
24
25func (n eofReader) Close() error { return nil }
26
27func (n eofReader) Read([]byte) (int, error) { return 0, io.EOF }
28
29type dumpTest struct {
30	// Either Req or GetReq can be set/nil but not both.
31	Req    *http.Request
32	GetReq func() *http.Request
33
34	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
35
36	WantDump    string
37	WantDumpOut string
38	MustError   bool // if true, the test is expected to throw an error
39	NoBody      bool // if true, set DumpRequest{,Out} body to false
40}
41
42var dumpTests = []dumpTest{
43	// HTTP/1.1 => chunked coding; body; empty trailer
44	{
45		Req: &http.Request{
46			Method: "GET",
47			URL: &url.URL{
48				Scheme: "http",
49				Host:   "www.google.com",
50				Path:   "/search",
51			},
52			ProtoMajor:       1,
53			ProtoMinor:       1,
54			TransferEncoding: []string{"chunked"},
55		},
56
57		Body: []byte("abcdef"),
58
59		WantDump: "GET /search HTTP/1.1\r\n" +
60			"Host: www.google.com\r\n" +
61			"Transfer-Encoding: chunked\r\n\r\n" +
62			chunk("abcdef") + chunk(""),
63	},
64
65	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
66	// and doesn't add a User-Agent.
67	{
68		Req: &http.Request{
69			Method:     "GET",
70			URL:        mustParseURL("/foo"),
71			ProtoMajor: 1,
72			ProtoMinor: 0,
73			Header: http.Header{
74				"X-Foo": []string{"X-Bar"},
75			},
76		},
77
78		WantDump: "GET /foo HTTP/1.0\r\n" +
79			"X-Foo: X-Bar\r\n\r\n",
80	},
81
82	{
83		Req: mustNewRequest("GET", "http://example.com/foo", nil),
84
85		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
86			"Host: example.com\r\n" +
87			"User-Agent: Go-http-client/1.1\r\n" +
88			"Accept-Encoding: gzip\r\n\r\n",
89	},
90
91	// Test that an https URL doesn't try to do an SSL negotiation
92	// with a bytes.Buffer and hang with all goroutines not
93	// runnable.
94	{
95		Req: mustNewRequest("GET", "https://example.com/foo", nil),
96		WantDumpOut: "GET /foo HTTP/1.1\r\n" +
97			"Host: example.com\r\n" +
98			"User-Agent: Go-http-client/1.1\r\n" +
99			"Accept-Encoding: gzip\r\n\r\n",
100	},
101
102	// Request with Body, but Dump requested without it.
103	{
104		Req: &http.Request{
105			Method: "POST",
106			URL: &url.URL{
107				Scheme: "http",
108				Host:   "post.tld",
109				Path:   "/",
110			},
111			ContentLength: 6,
112			ProtoMajor:    1,
113			ProtoMinor:    1,
114		},
115
116		Body: []byte("abcdef"),
117
118		WantDumpOut: "POST / HTTP/1.1\r\n" +
119			"Host: post.tld\r\n" +
120			"User-Agent: Go-http-client/1.1\r\n" +
121			"Content-Length: 6\r\n" +
122			"Accept-Encoding: gzip\r\n\r\n",
123
124		NoBody: true,
125	},
126
127	// Request with Body > 8196 (default buffer size)
128	{
129		Req: &http.Request{
130			Method: "POST",
131			URL: &url.URL{
132				Scheme: "http",
133				Host:   "post.tld",
134				Path:   "/",
135			},
136			Header: http.Header{
137				"Content-Length": []string{"8193"},
138			},
139
140			ContentLength: 8193,
141			ProtoMajor:    1,
142			ProtoMinor:    1,
143		},
144
145		Body: bytes.Repeat([]byte("a"), 8193),
146
147		WantDumpOut: "POST / HTTP/1.1\r\n" +
148			"Host: post.tld\r\n" +
149			"User-Agent: Go-http-client/1.1\r\n" +
150			"Content-Length: 8193\r\n" +
151			"Accept-Encoding: gzip\r\n\r\n" +
152			strings.Repeat("a", 8193),
153		WantDump: "POST / HTTP/1.1\r\n" +
154			"Host: post.tld\r\n" +
155			"Content-Length: 8193\r\n\r\n" +
156			strings.Repeat("a", 8193),
157	},
158
159	{
160		GetReq: func() *http.Request {
161			return mustReadRequest("GET http://foo.com/ HTTP/1.1\r\n" +
162				"User-Agent: blah\r\n\r\n")
163		},
164		NoBody: true,
165		WantDump: "GET http://foo.com/ HTTP/1.1\r\n" +
166			"User-Agent: blah\r\n\r\n",
167	},
168
169	// Issue #7215. DumpRequest should return the "Content-Length" when set
170	{
171		GetReq: func() *http.Request {
172			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
173				"Host: passport.myhost.com\r\n" +
174				"Content-Length: 3\r\n" +
175				"\r\nkey1=name1&key2=name2")
176		},
177		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
178			"Host: passport.myhost.com\r\n" +
179			"Content-Length: 3\r\n" +
180			"\r\nkey",
181	},
182	// Issue #7215. DumpRequest should return the "Content-Length" in ReadRequest
183	{
184		GetReq: func() *http.Request {
185			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
186				"Host: passport.myhost.com\r\n" +
187				"Content-Length: 0\r\n" +
188				"\r\nkey1=name1&key2=name2")
189		},
190		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
191			"Host: passport.myhost.com\r\n" +
192			"Content-Length: 0\r\n\r\n",
193	},
194
195	// Issue #7215. DumpRequest should not return the "Content-Length" if unset
196	{
197		GetReq: func() *http.Request {
198			return mustReadRequest("POST /v2/api/?login HTTP/1.1\r\n" +
199				"Host: passport.myhost.com\r\n" +
200				"\r\nkey1=name1&key2=name2")
201		},
202		WantDump: "POST /v2/api/?login HTTP/1.1\r\n" +
203			"Host: passport.myhost.com\r\n\r\n",
204	},
205
206	// Issue 18506: make drainBody recognize NoBody. Otherwise
207	// this was turning into a chunked request.
208	{
209		Req: mustNewRequest("POST", "http://example.com/foo", http.NoBody),
210		WantDumpOut: "POST /foo HTTP/1.1\r\n" +
211			"Host: example.com\r\n" +
212			"User-Agent: Go-http-client/1.1\r\n" +
213			"Content-Length: 0\r\n" +
214			"Accept-Encoding: gzip\r\n\r\n",
215	},
216
217	// Issue 34504: a non-nil Body without ContentLength set should be chunked
218	{
219		Req: &http.Request{
220			Method: "PUT",
221			URL: &url.URL{
222				Scheme: "http",
223				Host:   "post.tld",
224				Path:   "/test",
225			},
226			ContentLength: 0,
227			Proto:         "HTTP/1.1",
228			ProtoMajor:    1,
229			ProtoMinor:    1,
230			Body:          &eofReader{},
231		},
232		NoBody: true,
233		WantDumpOut: "PUT /test HTTP/1.1\r\n" +
234			"Host: post.tld\r\n" +
235			"User-Agent: Go-http-client/1.1\r\n" +
236			"Transfer-Encoding: chunked\r\n" +
237			"Accept-Encoding: gzip\r\n\r\n",
238	},
239}
240
241func TestDumpRequest(t *testing.T) {
242	// Make a copy of dumpTests and add 10 new cases with an empty URL
243	// to test that no goroutines are leaked. See golang.org/issue/32571.
244	// 10 seems to be a decent number which always triggers the failure.
245	dumpTests := dumpTests[:]
246	for i := 0; i < 10; i++ {
247		dumpTests = append(dumpTests, dumpTest{
248			Req:       mustNewRequest("GET", "", nil),
249			MustError: true,
250		})
251	}
252	numg0 := runtime.NumGoroutine()
253	for i, tt := range dumpTests {
254		if tt.Req != nil && tt.GetReq != nil || tt.Req == nil && tt.GetReq == nil {
255			t.Errorf("#%d: either .Req(%p) or .GetReq(%p) can be set/nil but not both", i, tt.Req, tt.GetReq)
256			continue
257		}
258
259		freshReq := func(ti dumpTest) *http.Request {
260			req := ti.Req
261			if req == nil {
262				req = ti.GetReq()
263			}
264
265			if req.Header == nil {
266				req.Header = make(http.Header)
267			}
268
269			if ti.Body == nil {
270				return req
271			}
272			switch b := ti.Body.(type) {
273			case []byte:
274				req.Body = io.NopCloser(bytes.NewReader(b))
275			case func() io.ReadCloser:
276				req.Body = b()
277			default:
278				t.Fatalf("Test %d: unsupported Body of %T", i, ti.Body)
279			}
280			return req
281		}
282
283		if tt.WantDump != "" {
284			req := freshReq(tt)
285			dump, err := DumpRequest(req, !tt.NoBody)
286			if err != nil {
287				t.Errorf("DumpRequest #%d: %s\nWantDump:\n%s", i, err, tt.WantDump)
288				continue
289			}
290			if string(dump) != tt.WantDump {
291				t.Errorf("DumpRequest %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDump, string(dump))
292				continue
293			}
294		}
295
296		if tt.MustError {
297			req := freshReq(tt)
298			_, err := DumpRequestOut(req, !tt.NoBody)
299			if err == nil {
300				t.Errorf("DumpRequestOut #%d: expected an error, got nil", i)
301			}
302			continue
303		}
304
305		if tt.WantDumpOut != "" {
306			req := freshReq(tt)
307			dump, err := DumpRequestOut(req, !tt.NoBody)
308			if err != nil {
309				t.Errorf("DumpRequestOut #%d: %s", i, err)
310				continue
311			}
312			if string(dump) != tt.WantDumpOut {
313				t.Errorf("DumpRequestOut %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantDumpOut, string(dump))
314				continue
315			}
316		}
317	}
318
319	// Validate we haven't leaked any goroutines.
320	var dg int
321	dl := deadline(t, 5*time.Second, time.Second)
322	for time.Now().Before(dl) {
323		if dg = runtime.NumGoroutine() - numg0; dg <= 4 {
324			// No unexpected goroutines.
325			return
326		}
327
328		// Allow goroutines to schedule and die off.
329		runtime.Gosched()
330	}
331
332	buf := make([]byte, 4096)
333	buf = buf[:runtime.Stack(buf, true)]
334	t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf)
335}
336
337// deadline returns the time which is needed before t.Deadline()
338// if one is configured and it is s greater than needed in the future,
339// otherwise defaultDelay from the current time.
340func deadline(t *testing.T, defaultDelay, needed time.Duration) time.Time {
341	if dl, ok := t.Deadline(); ok {
342		if dl = dl.Add(-needed); dl.After(time.Now()) {
343			// Allow an arbitrarily long delay.
344			return dl
345		}
346	}
347
348	// No deadline configured or its closer than needed from now
349	// so just use the default.
350	return time.Now().Add(defaultDelay)
351}
352
353func chunk(s string) string {
354	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
355}
356
357func mustParseURL(s string) *url.URL {
358	u, err := url.Parse(s)
359	if err != nil {
360		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
361	}
362	return u
363}
364
365func mustNewRequest(method, url string, body io.Reader) *http.Request {
366	req, err := http.NewRequest(method, url, body)
367	if err != nil {
368		panic(fmt.Sprintf("NewRequest(%q, %q, %p) err = %v", method, url, body, err))
369	}
370	return req
371}
372
373func mustReadRequest(s string) *http.Request {
374	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(s)))
375	if err != nil {
376		panic(err)
377	}
378	return req
379}
380
381var dumpResTests = []struct {
382	res  *http.Response
383	body bool
384	want string
385}{
386	{
387		res: &http.Response{
388			Status:        "200 OK",
389			StatusCode:    200,
390			Proto:         "HTTP/1.1",
391			ProtoMajor:    1,
392			ProtoMinor:    1,
393			ContentLength: 50,
394			Header: http.Header{
395				"Foo": []string{"Bar"},
396			},
397			Body: io.NopCloser(strings.NewReader("foo")), // shouldn't be used
398		},
399		body: false, // to verify we see 50, not empty or 3.
400		want: `HTTP/1.1 200 OK
401Content-Length: 50
402Foo: Bar`,
403	},
404
405	{
406		res: &http.Response{
407			Status:        "200 OK",
408			StatusCode:    200,
409			Proto:         "HTTP/1.1",
410			ProtoMajor:    1,
411			ProtoMinor:    1,
412			ContentLength: 3,
413			Body:          io.NopCloser(strings.NewReader("foo")),
414		},
415		body: true,
416		want: `HTTP/1.1 200 OK
417Content-Length: 3
418
419foo`,
420	},
421
422	{
423		res: &http.Response{
424			Status:           "200 OK",
425			StatusCode:       200,
426			Proto:            "HTTP/1.1",
427			ProtoMajor:       1,
428			ProtoMinor:       1,
429			ContentLength:    -1,
430			Body:             io.NopCloser(strings.NewReader("foo")),
431			TransferEncoding: []string{"chunked"},
432		},
433		body: true,
434		want: `HTTP/1.1 200 OK
435Transfer-Encoding: chunked
436
4373
438foo
4390`,
440	},
441	{
442		res: &http.Response{
443			Status:        "200 OK",
444			StatusCode:    200,
445			Proto:         "HTTP/1.1",
446			ProtoMajor:    1,
447			ProtoMinor:    1,
448			ContentLength: 0,
449			Header: http.Header{
450				// To verify if headers are not filtered out.
451				"Foo1": []string{"Bar1"},
452				"Foo2": []string{"Bar2"},
453			},
454			Body: nil,
455		},
456		body: false, // to verify we see 0, not empty.
457		want: `HTTP/1.1 200 OK
458Foo1: Bar1
459Foo2: Bar2
460Content-Length: 0`,
461	},
462}
463
464func TestDumpResponse(t *testing.T) {
465	for i, tt := range dumpResTests {
466		gotb, err := DumpResponse(tt.res, tt.body)
467		if err != nil {
468			t.Errorf("%d. DumpResponse = %v", i, err)
469			continue
470		}
471		got := string(gotb)
472		got = strings.TrimSpace(got)
473		got = strings.ReplaceAll(got, "\r", "")
474
475		if got != tt.want {
476			t.Errorf("%d.\nDumpResponse got:\n%s\n\nWant:\n%s\n", i, got, tt.want)
477		}
478	}
479}
480
481// Issue 38352: Check for deadlock on canceled requests.
482func TestDumpRequestOutIssue38352(t *testing.T) {
483	if testing.Short() {
484		return
485	}
486	t.Parallel()
487
488	timeout := 10 * time.Second
489	if deadline, ok := t.Deadline(); ok {
490		timeout = time.Until(deadline)
491		timeout -= time.Second * 2 // Leave 2 seconds to report failures.
492	}
493	for i := 0; i < 1000; i++ {
494		delay := time.Duration(rand.Intn(5)) * time.Millisecond
495		ctx, cancel := context.WithTimeout(context.Background(), delay)
496		defer cancel()
497
498		r := bytes.NewBuffer(make([]byte, 10000))
499		req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", r)
500		if err != nil {
501			t.Fatal(err)
502		}
503
504		out := make(chan error)
505		go func() {
506			_, err = DumpRequestOut(req, true)
507			out <- err
508		}()
509
510		select {
511		case <-out:
512		case <-time.After(timeout):
513			b := &bytes.Buffer{}
514			fmt.Fprintf(b, "deadlock detected on iteration %d after %s with delay: %v\n", i, timeout, delay)
515			pprof.Lookup("goroutine").WriteTo(b, 1)
516			t.Fatal(b.String())
517		}
518	}
519}
520