1// Copyright 2010 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 http
6
7import (
8	"bytes"
9	"errors"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net/url"
14	"strings"
15	"testing"
16)
17
18type reqWriteTest struct {
19	Req  Request
20	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
21
22	// Any of these three may be empty to skip that test.
23	WantWrite string // Request.Write
24	WantProxy string // Request.WriteProxy
25
26	WantError error // wanted error from Request.Write
27}
28
29var reqWriteTests = []reqWriteTest{
30	// HTTP/1.1 => chunked coding; no body; no trailer
31	{
32		Req: Request{
33			Method: "GET",
34			URL: &url.URL{
35				Scheme: "http",
36				Host:   "www.techcrunch.com",
37				Path:   "/",
38			},
39			Proto:      "HTTP/1.1",
40			ProtoMajor: 1,
41			ProtoMinor: 1,
42			Header: Header{
43				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
44				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
45				"Accept-Encoding":  {"gzip,deflate"},
46				"Accept-Language":  {"en-us,en;q=0.5"},
47				"Keep-Alive":       {"300"},
48				"Proxy-Connection": {"keep-alive"},
49				"User-Agent":       {"Fake"},
50			},
51			Body:  nil,
52			Close: false,
53			Host:  "www.techcrunch.com",
54			Form:  map[string][]string{},
55		},
56
57		WantWrite: "GET / HTTP/1.1\r\n" +
58			"Host: www.techcrunch.com\r\n" +
59			"User-Agent: Fake\r\n" +
60			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
61			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
62			"Accept-Encoding: gzip,deflate\r\n" +
63			"Accept-Language: en-us,en;q=0.5\r\n" +
64			"Keep-Alive: 300\r\n" +
65			"Proxy-Connection: keep-alive\r\n\r\n",
66
67		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
68			"Host: www.techcrunch.com\r\n" +
69			"User-Agent: Fake\r\n" +
70			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
71			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
72			"Accept-Encoding: gzip,deflate\r\n" +
73			"Accept-Language: en-us,en;q=0.5\r\n" +
74			"Keep-Alive: 300\r\n" +
75			"Proxy-Connection: keep-alive\r\n\r\n",
76	},
77	// HTTP/1.1 => chunked coding; body; empty trailer
78	{
79		Req: Request{
80			Method: "GET",
81			URL: &url.URL{
82				Scheme: "http",
83				Host:   "www.google.com",
84				Path:   "/search",
85			},
86			ProtoMajor:       1,
87			ProtoMinor:       1,
88			Header:           Header{},
89			TransferEncoding: []string{"chunked"},
90		},
91
92		Body: []byte("abcdef"),
93
94		WantWrite: "GET /search HTTP/1.1\r\n" +
95			"Host: www.google.com\r\n" +
96			"User-Agent: Go-http-client/1.1\r\n" +
97			"Transfer-Encoding: chunked\r\n\r\n" +
98			chunk("abcdef") + chunk(""),
99
100		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
101			"Host: www.google.com\r\n" +
102			"User-Agent: Go-http-client/1.1\r\n" +
103			"Transfer-Encoding: chunked\r\n\r\n" +
104			chunk("abcdef") + chunk(""),
105	},
106	// HTTP/1.1 POST => chunked coding; body; empty trailer
107	{
108		Req: Request{
109			Method: "POST",
110			URL: &url.URL{
111				Scheme: "http",
112				Host:   "www.google.com",
113				Path:   "/search",
114			},
115			ProtoMajor:       1,
116			ProtoMinor:       1,
117			Header:           Header{},
118			Close:            true,
119			TransferEncoding: []string{"chunked"},
120		},
121
122		Body: []byte("abcdef"),
123
124		WantWrite: "POST /search HTTP/1.1\r\n" +
125			"Host: www.google.com\r\n" +
126			"User-Agent: Go-http-client/1.1\r\n" +
127			"Connection: close\r\n" +
128			"Transfer-Encoding: chunked\r\n\r\n" +
129			chunk("abcdef") + chunk(""),
130
131		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
132			"Host: www.google.com\r\n" +
133			"User-Agent: Go-http-client/1.1\r\n" +
134			"Connection: close\r\n" +
135			"Transfer-Encoding: chunked\r\n\r\n" +
136			chunk("abcdef") + chunk(""),
137	},
138
139	// HTTP/1.1 POST with Content-Length, no chunking
140	{
141		Req: Request{
142			Method: "POST",
143			URL: &url.URL{
144				Scheme: "http",
145				Host:   "www.google.com",
146				Path:   "/search",
147			},
148			ProtoMajor:    1,
149			ProtoMinor:    1,
150			Header:        Header{},
151			Close:         true,
152			ContentLength: 6,
153		},
154
155		Body: []byte("abcdef"),
156
157		WantWrite: "POST /search HTTP/1.1\r\n" +
158			"Host: www.google.com\r\n" +
159			"User-Agent: Go-http-client/1.1\r\n" +
160			"Connection: close\r\n" +
161			"Content-Length: 6\r\n" +
162			"\r\n" +
163			"abcdef",
164
165		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
166			"Host: www.google.com\r\n" +
167			"User-Agent: Go-http-client/1.1\r\n" +
168			"Connection: close\r\n" +
169			"Content-Length: 6\r\n" +
170			"\r\n" +
171			"abcdef",
172	},
173
174	// HTTP/1.1 POST with Content-Length in headers
175	{
176		Req: Request{
177			Method: "POST",
178			URL:    mustParseURL("http://example.com/"),
179			Host:   "example.com",
180			Header: Header{
181				"Content-Length": []string{"10"}, // ignored
182			},
183			ContentLength: 6,
184		},
185
186		Body: []byte("abcdef"),
187
188		WantWrite: "POST / HTTP/1.1\r\n" +
189			"Host: example.com\r\n" +
190			"User-Agent: Go-http-client/1.1\r\n" +
191			"Content-Length: 6\r\n" +
192			"\r\n" +
193			"abcdef",
194
195		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
196			"Host: example.com\r\n" +
197			"User-Agent: Go-http-client/1.1\r\n" +
198			"Content-Length: 6\r\n" +
199			"\r\n" +
200			"abcdef",
201	},
202
203	// default to HTTP/1.1
204	{
205		Req: Request{
206			Method: "GET",
207			URL:    mustParseURL("/search"),
208			Host:   "www.google.com",
209		},
210
211		WantWrite: "GET /search HTTP/1.1\r\n" +
212			"Host: www.google.com\r\n" +
213			"User-Agent: Go-http-client/1.1\r\n" +
214			"\r\n",
215	},
216
217	// Request with a 0 ContentLength and a 0 byte body.
218	{
219		Req: Request{
220			Method:        "POST",
221			URL:           mustParseURL("/"),
222			Host:          "example.com",
223			ProtoMajor:    1,
224			ProtoMinor:    1,
225			ContentLength: 0, // as if unset by user
226		},
227
228		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
229
230		// RFC 2616 Section 14.13 says Content-Length should be specified
231		// unless body is prohibited by the request method.
232		// Also, nginx expects it for POST and PUT.
233		WantWrite: "POST / HTTP/1.1\r\n" +
234			"Host: example.com\r\n" +
235			"User-Agent: Go-http-client/1.1\r\n" +
236			"Content-Length: 0\r\n" +
237			"\r\n",
238
239		WantProxy: "POST / HTTP/1.1\r\n" +
240			"Host: example.com\r\n" +
241			"User-Agent: Go-http-client/1.1\r\n" +
242			"Content-Length: 0\r\n" +
243			"\r\n",
244	},
245
246	// Request with a 0 ContentLength and a 1 byte body.
247	{
248		Req: Request{
249			Method:        "POST",
250			URL:           mustParseURL("/"),
251			Host:          "example.com",
252			ProtoMajor:    1,
253			ProtoMinor:    1,
254			ContentLength: 0, // as if unset by user
255		},
256
257		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
258
259		WantWrite: "POST / HTTP/1.1\r\n" +
260			"Host: example.com\r\n" +
261			"User-Agent: Go-http-client/1.1\r\n" +
262			"Transfer-Encoding: chunked\r\n\r\n" +
263			chunk("x") + chunk(""),
264
265		WantProxy: "POST / HTTP/1.1\r\n" +
266			"Host: example.com\r\n" +
267			"User-Agent: Go-http-client/1.1\r\n" +
268			"Transfer-Encoding: chunked\r\n\r\n" +
269			chunk("x") + chunk(""),
270	},
271
272	// Request with a ContentLength of 10 but a 5 byte body.
273	{
274		Req: Request{
275			Method:        "POST",
276			URL:           mustParseURL("/"),
277			Host:          "example.com",
278			ProtoMajor:    1,
279			ProtoMinor:    1,
280			ContentLength: 10, // but we're going to send only 5 bytes
281		},
282		Body:      []byte("12345"),
283		WantError: errors.New("http: ContentLength=10 with Body length 5"),
284	},
285
286	// Request with a ContentLength of 4 but an 8 byte body.
287	{
288		Req: Request{
289			Method:        "POST",
290			URL:           mustParseURL("/"),
291			Host:          "example.com",
292			ProtoMajor:    1,
293			ProtoMinor:    1,
294			ContentLength: 4, // but we're going to try to send 8 bytes
295		},
296		Body:      []byte("12345678"),
297		WantError: errors.New("http: ContentLength=4 with Body length 8"),
298	},
299
300	// Request with a 5 ContentLength and nil body.
301	{
302		Req: Request{
303			Method:        "POST",
304			URL:           mustParseURL("/"),
305			Host:          "example.com",
306			ProtoMajor:    1,
307			ProtoMinor:    1,
308			ContentLength: 5, // but we'll omit the body
309		},
310		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
311	},
312
313	// Request with a 0 ContentLength and a body with 1 byte content and an error.
314	{
315		Req: Request{
316			Method:        "POST",
317			URL:           mustParseURL("/"),
318			Host:          "example.com",
319			ProtoMajor:    1,
320			ProtoMinor:    1,
321			ContentLength: 0, // as if unset by user
322		},
323
324		Body: func() io.ReadCloser {
325			err := errors.New("Custom reader error")
326			errReader := &errorReader{err}
327			return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
328		},
329
330		WantError: errors.New("Custom reader error"),
331	},
332
333	// Request with a 0 ContentLength and a body without content and an error.
334	{
335		Req: Request{
336			Method:        "POST",
337			URL:           mustParseURL("/"),
338			Host:          "example.com",
339			ProtoMajor:    1,
340			ProtoMinor:    1,
341			ContentLength: 0, // as if unset by user
342		},
343
344		Body: func() io.ReadCloser {
345			err := errors.New("Custom reader error")
346			errReader := &errorReader{err}
347			return ioutil.NopCloser(errReader)
348		},
349
350		WantError: errors.New("Custom reader error"),
351	},
352
353	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
354	// and doesn't add a User-Agent.
355	{
356		Req: Request{
357			Method:     "GET",
358			URL:        mustParseURL("/foo"),
359			ProtoMajor: 1,
360			ProtoMinor: 0,
361			Header: Header{
362				"X-Foo": []string{"X-Bar"},
363			},
364		},
365
366		WantWrite: "GET /foo HTTP/1.1\r\n" +
367			"Host: \r\n" +
368			"User-Agent: Go-http-client/1.1\r\n" +
369			"X-Foo: X-Bar\r\n\r\n",
370	},
371
372	// If no Request.Host and no Request.URL.Host, we send
373	// an empty Host header, and don't use
374	// Request.Header["Host"]. This is just testing that
375	// we don't change Go 1.0 behavior.
376	{
377		Req: Request{
378			Method: "GET",
379			Host:   "",
380			URL: &url.URL{
381				Scheme: "http",
382				Host:   "",
383				Path:   "/search",
384			},
385			ProtoMajor: 1,
386			ProtoMinor: 1,
387			Header: Header{
388				"Host": []string{"bad.example.com"},
389			},
390		},
391
392		WantWrite: "GET /search HTTP/1.1\r\n" +
393			"Host: \r\n" +
394			"User-Agent: Go-http-client/1.1\r\n\r\n",
395	},
396
397	// Opaque test #1 from golang.org/issue/4860
398	{
399		Req: Request{
400			Method: "GET",
401			URL: &url.URL{
402				Scheme: "http",
403				Host:   "www.google.com",
404				Opaque: "/%2F/%2F/",
405			},
406			ProtoMajor: 1,
407			ProtoMinor: 1,
408			Header:     Header{},
409		},
410
411		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
412			"Host: www.google.com\r\n" +
413			"User-Agent: Go-http-client/1.1\r\n\r\n",
414	},
415
416	// Opaque test #2 from golang.org/issue/4860
417	{
418		Req: Request{
419			Method: "GET",
420			URL: &url.URL{
421				Scheme: "http",
422				Host:   "x.google.com",
423				Opaque: "//y.google.com/%2F/%2F/",
424			},
425			ProtoMajor: 1,
426			ProtoMinor: 1,
427			Header:     Header{},
428		},
429
430		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
431			"Host: x.google.com\r\n" +
432			"User-Agent: Go-http-client/1.1\r\n\r\n",
433	},
434
435	// Testing custom case in header keys. Issue 5022.
436	{
437		Req: Request{
438			Method: "GET",
439			URL: &url.URL{
440				Scheme: "http",
441				Host:   "www.google.com",
442				Path:   "/",
443			},
444			Proto:      "HTTP/1.1",
445			ProtoMajor: 1,
446			ProtoMinor: 1,
447			Header: Header{
448				"ALL-CAPS": {"x"},
449			},
450		},
451
452		WantWrite: "GET / HTTP/1.1\r\n" +
453			"Host: www.google.com\r\n" +
454			"User-Agent: Go-http-client/1.1\r\n" +
455			"ALL-CAPS: x\r\n" +
456			"\r\n",
457	},
458
459	// Request with host header field; IPv6 address with zone identifier
460	{
461		Req: Request{
462			Method: "GET",
463			URL: &url.URL{
464				Host: "[fe80::1%en0]",
465			},
466		},
467
468		WantWrite: "GET / HTTP/1.1\r\n" +
469			"Host: [fe80::1]\r\n" +
470			"User-Agent: Go-http-client/1.1\r\n" +
471			"\r\n",
472	},
473
474	// Request with optional host header field; IPv6 address with zone identifier
475	{
476		Req: Request{
477			Method: "GET",
478			URL: &url.URL{
479				Host: "www.example.com",
480			},
481			Host: "[fe80::1%en0]:8080",
482		},
483
484		WantWrite: "GET / HTTP/1.1\r\n" +
485			"Host: [fe80::1]:8080\r\n" +
486			"User-Agent: Go-http-client/1.1\r\n" +
487			"\r\n",
488	},
489}
490
491func TestRequestWrite(t *testing.T) {
492	for i := range reqWriteTests {
493		tt := &reqWriteTests[i]
494
495		setBody := func() {
496			if tt.Body == nil {
497				return
498			}
499			switch b := tt.Body.(type) {
500			case []byte:
501				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
502			case func() io.ReadCloser:
503				tt.Req.Body = b()
504			}
505		}
506		setBody()
507		if tt.Req.Header == nil {
508			tt.Req.Header = make(Header)
509		}
510
511		var braw bytes.Buffer
512		err := tt.Req.Write(&braw)
513		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
514			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
515			continue
516		}
517		if err != nil {
518			continue
519		}
520
521		if tt.WantWrite != "" {
522			sraw := braw.String()
523			if sraw != tt.WantWrite {
524				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
525				continue
526			}
527		}
528
529		if tt.WantProxy != "" {
530			setBody()
531			var praw bytes.Buffer
532			err = tt.Req.WriteProxy(&praw)
533			if err != nil {
534				t.Errorf("WriteProxy #%d: %s", i, err)
535				continue
536			}
537			sraw := praw.String()
538			if sraw != tt.WantProxy {
539				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
540				continue
541			}
542		}
543	}
544}
545
546type closeChecker struct {
547	io.Reader
548	closed bool
549}
550
551func (rc *closeChecker) Close() error {
552	rc.closed = true
553	return nil
554}
555
556// TestRequestWriteClosesBody tests that Request.Write does close its request.Body.
557// It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
558// inside a NopCloser, and that it serializes it correctly.
559func TestRequestWriteClosesBody(t *testing.T) {
560	rc := &closeChecker{Reader: strings.NewReader("my body")}
561	req, _ := NewRequest("POST", "http://foo.com/", rc)
562	if req.ContentLength != 0 {
563		t.Errorf("got req.ContentLength %d, want 0", req.ContentLength)
564	}
565	buf := new(bytes.Buffer)
566	req.Write(buf)
567	if !rc.closed {
568		t.Error("body not closed after write")
569	}
570	expected := "POST / HTTP/1.1\r\n" +
571		"Host: foo.com\r\n" +
572		"User-Agent: Go-http-client/1.1\r\n" +
573		"Transfer-Encoding: chunked\r\n\r\n" +
574		// TODO: currently we don't buffer before chunking, so we get a
575		// single "m" chunk before the other chunks, as this was the 1-byte
576		// read from our MultiReader where we stiched the Body back together
577		// after sniffing whether the Body was 0 bytes or not.
578		chunk("m") +
579		chunk("y body") +
580		chunk("")
581	if buf.String() != expected {
582		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
583	}
584}
585
586func chunk(s string) string {
587	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
588}
589
590func mustParseURL(s string) *url.URL {
591	u, err := url.Parse(s)
592	if err != nil {
593		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
594	}
595	return u
596}
597
598type writerFunc func([]byte) (int, error)
599
600func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
601
602// TestRequestWriteError tests the Write err != nil checks in (*Request).write.
603func TestRequestWriteError(t *testing.T) {
604	failAfter, writeCount := 0, 0
605	errFail := errors.New("fake write failure")
606
607	// w is the buffered io.Writer to write the request to.  It
608	// fails exactly once on its Nth Write call, as controlled by
609	// failAfter. It also tracks the number of calls in
610	// writeCount.
611	w := struct {
612		io.ByteWriter // to avoid being wrapped by a bufio.Writer
613		io.Writer
614	}{
615		nil,
616		writerFunc(func(p []byte) (n int, err error) {
617			writeCount++
618			if failAfter == 0 {
619				err = errFail
620			}
621			failAfter--
622			return len(p), err
623		}),
624	}
625
626	req, _ := NewRequest("GET", "http://example.com/", nil)
627	const writeCalls = 4 // number of Write calls in current implementation
628	sawGood := false
629	for n := 0; n <= writeCalls+2; n++ {
630		failAfter = n
631		writeCount = 0
632		err := req.Write(w)
633		var wantErr error
634		if n < writeCalls {
635			wantErr = errFail
636		}
637		if err != wantErr {
638			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
639			continue
640		}
641		if err == nil {
642			sawGood = true
643			if writeCount != writeCalls {
644				t.Fatalf("writeCalls constant is outdated in test")
645			}
646		}
647		if writeCount > writeCalls || writeCount > n+1 {
648			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
649		}
650	}
651	if !sawGood {
652		t.Fatalf("writeCalls constant is outdated in test")
653	}
654}
655