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	"bufio"
9	"bytes"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net/url"
14	"reflect"
15	"strings"
16	"testing"
17)
18
19type reqTest struct {
20	Raw     string
21	Req     *Request
22	Body    string
23	Trailer Header
24	Error   string
25}
26
27var noError = ""
28var noBodyStr = ""
29var noTrailer Header = nil
30
31var reqTests = []reqTest{
32	// Baseline test; All Request fields included for template use
33	{
34		"GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
35			"Host: www.techcrunch.com\r\n" +
36			"User-Agent: Fake\r\n" +
37			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
38			"Accept-Language: en-us,en;q=0.5\r\n" +
39			"Accept-Encoding: gzip,deflate\r\n" +
40			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
41			"Keep-Alive: 300\r\n" +
42			"Content-Length: 7\r\n" +
43			"Proxy-Connection: keep-alive\r\n\r\n" +
44			"abcdef\n???",
45
46		&Request{
47			Method: "GET",
48			URL: &url.URL{
49				Scheme: "http",
50				Host:   "www.techcrunch.com",
51				Path:   "/",
52			},
53			Proto:      "HTTP/1.1",
54			ProtoMajor: 1,
55			ProtoMinor: 1,
56			Header: Header{
57				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
58				"Accept-Language":  {"en-us,en;q=0.5"},
59				"Accept-Encoding":  {"gzip,deflate"},
60				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
61				"Keep-Alive":       {"300"},
62				"Proxy-Connection": {"keep-alive"},
63				"Content-Length":   {"7"},
64				"User-Agent":       {"Fake"},
65			},
66			Close:         false,
67			ContentLength: 7,
68			Host:          "www.techcrunch.com",
69			RequestURI:    "http://www.techcrunch.com/",
70		},
71
72		"abcdef\n",
73
74		noTrailer,
75		noError,
76	},
77
78	// GET request with no body (the normal case)
79	{
80		"GET / HTTP/1.1\r\n" +
81			"Host: foo.com\r\n\r\n",
82
83		&Request{
84			Method: "GET",
85			URL: &url.URL{
86				Path: "/",
87			},
88			Proto:         "HTTP/1.1",
89			ProtoMajor:    1,
90			ProtoMinor:    1,
91			Header:        Header{},
92			Close:         false,
93			ContentLength: 0,
94			Host:          "foo.com",
95			RequestURI:    "/",
96		},
97
98		noBodyStr,
99		noTrailer,
100		noError,
101	},
102
103	// Tests that we don't parse a path that looks like a
104	// scheme-relative URI as a scheme-relative URI.
105	{
106		"GET //user@host/is/actually/a/path/ HTTP/1.1\r\n" +
107			"Host: test\r\n\r\n",
108
109		&Request{
110			Method: "GET",
111			URL: &url.URL{
112				Path: "//user@host/is/actually/a/path/",
113			},
114			Proto:         "HTTP/1.1",
115			ProtoMajor:    1,
116			ProtoMinor:    1,
117			Header:        Header{},
118			Close:         false,
119			ContentLength: 0,
120			Host:          "test",
121			RequestURI:    "//user@host/is/actually/a/path/",
122		},
123
124		noBodyStr,
125		noTrailer,
126		noError,
127	},
128
129	// Tests a bogus abs_path on the Request-Line (RFC 2616 section 5.1.2)
130	{
131		"GET ../../../../etc/passwd HTTP/1.1\r\n" +
132			"Host: test\r\n\r\n",
133		nil,
134		noBodyStr,
135		noTrailer,
136		"parse ../../../../etc/passwd: invalid URI for request",
137	},
138
139	// Tests missing URL:
140	{
141		"GET  HTTP/1.1\r\n" +
142			"Host: test\r\n\r\n",
143		nil,
144		noBodyStr,
145		noTrailer,
146		"parse : empty url",
147	},
148
149	// Tests chunked body with trailer:
150	{
151		"POST / HTTP/1.1\r\n" +
152			"Host: foo.com\r\n" +
153			"Transfer-Encoding: chunked\r\n\r\n" +
154			"3\r\nfoo\r\n" +
155			"3\r\nbar\r\n" +
156			"0\r\n" +
157			"Trailer-Key: Trailer-Value\r\n" +
158			"\r\n",
159		&Request{
160			Method: "POST",
161			URL: &url.URL{
162				Path: "/",
163			},
164			TransferEncoding: []string{"chunked"},
165			Proto:            "HTTP/1.1",
166			ProtoMajor:       1,
167			ProtoMinor:       1,
168			Header:           Header{},
169			ContentLength:    -1,
170			Host:             "foo.com",
171			RequestURI:       "/",
172		},
173
174		"foobar",
175		Header{
176			"Trailer-Key": {"Trailer-Value"},
177		},
178		noError,
179	},
180
181	// Tests chunked body and a bogus Content-Length which should be deleted.
182	{
183		"POST / HTTP/1.1\r\n" +
184			"Host: foo.com\r\n" +
185			"Transfer-Encoding: chunked\r\n" +
186			"Content-Length: 9999\r\n\r\n" + // to be removed.
187			"3\r\nfoo\r\n" +
188			"3\r\nbar\r\n" +
189			"0\r\n" +
190			"\r\n",
191		&Request{
192			Method: "POST",
193			URL: &url.URL{
194				Path: "/",
195			},
196			TransferEncoding: []string{"chunked"},
197			Proto:            "HTTP/1.1",
198			ProtoMajor:       1,
199			ProtoMinor:       1,
200			Header:           Header{},
201			ContentLength:    -1,
202			Host:             "foo.com",
203			RequestURI:       "/",
204		},
205
206		"foobar",
207		noTrailer,
208		noError,
209	},
210
211	// CONNECT request with domain name:
212	{
213		"CONNECT www.google.com:443 HTTP/1.1\r\n\r\n",
214
215		&Request{
216			Method: "CONNECT",
217			URL: &url.URL{
218				Host: "www.google.com:443",
219			},
220			Proto:         "HTTP/1.1",
221			ProtoMajor:    1,
222			ProtoMinor:    1,
223			Header:        Header{},
224			Close:         false,
225			ContentLength: 0,
226			Host:          "www.google.com:443",
227			RequestURI:    "www.google.com:443",
228		},
229
230		noBodyStr,
231		noTrailer,
232		noError,
233	},
234
235	// CONNECT request with IP address:
236	{
237		"CONNECT 127.0.0.1:6060 HTTP/1.1\r\n\r\n",
238
239		&Request{
240			Method: "CONNECT",
241			URL: &url.URL{
242				Host: "127.0.0.1:6060",
243			},
244			Proto:         "HTTP/1.1",
245			ProtoMajor:    1,
246			ProtoMinor:    1,
247			Header:        Header{},
248			Close:         false,
249			ContentLength: 0,
250			Host:          "127.0.0.1:6060",
251			RequestURI:    "127.0.0.1:6060",
252		},
253
254		noBodyStr,
255		noTrailer,
256		noError,
257	},
258
259	// CONNECT request for RPC:
260	{
261		"CONNECT /_goRPC_ HTTP/1.1\r\n\r\n",
262
263		&Request{
264			Method: "CONNECT",
265			URL: &url.URL{
266				Path: "/_goRPC_",
267			},
268			Proto:         "HTTP/1.1",
269			ProtoMajor:    1,
270			ProtoMinor:    1,
271			Header:        Header{},
272			Close:         false,
273			ContentLength: 0,
274			Host:          "",
275			RequestURI:    "/_goRPC_",
276		},
277
278		noBodyStr,
279		noTrailer,
280		noError,
281	},
282
283	// SSDP Notify request. golang.org/issue/3692
284	{
285		"NOTIFY * HTTP/1.1\r\nServer: foo\r\n\r\n",
286		&Request{
287			Method: "NOTIFY",
288			URL: &url.URL{
289				Path: "*",
290			},
291			Proto:      "HTTP/1.1",
292			ProtoMajor: 1,
293			ProtoMinor: 1,
294			Header: Header{
295				"Server": []string{"foo"},
296			},
297			Close:         false,
298			ContentLength: 0,
299			RequestURI:    "*",
300		},
301
302		noBodyStr,
303		noTrailer,
304		noError,
305	},
306
307	// OPTIONS request. Similar to golang.org/issue/3692
308	{
309		"OPTIONS * HTTP/1.1\r\nServer: foo\r\n\r\n",
310		&Request{
311			Method: "OPTIONS",
312			URL: &url.URL{
313				Path: "*",
314			},
315			Proto:      "HTTP/1.1",
316			ProtoMajor: 1,
317			ProtoMinor: 1,
318			Header: Header{
319				"Server": []string{"foo"},
320			},
321			Close:         false,
322			ContentLength: 0,
323			RequestURI:    "*",
324		},
325
326		noBodyStr,
327		noTrailer,
328		noError,
329	},
330
331	// Connection: close. golang.org/issue/8261
332	{
333		"GET / HTTP/1.1\r\nHost: issue8261.com\r\nConnection: close\r\n\r\n",
334		&Request{
335			Method: "GET",
336			URL: &url.URL{
337				Path: "/",
338			},
339			Header: Header{
340				// This wasn't removed from Go 1.0 to
341				// Go 1.3, so locking it in that we
342				// keep this:
343				"Connection": []string{"close"},
344			},
345			Host:       "issue8261.com",
346			Proto:      "HTTP/1.1",
347			ProtoMajor: 1,
348			ProtoMinor: 1,
349			Close:      true,
350			RequestURI: "/",
351		},
352
353		noBodyStr,
354		noTrailer,
355		noError,
356	},
357
358	// HEAD with Content-Length 0. Make sure this is permitted,
359	// since I think we used to send it.
360	{
361		"HEAD / HTTP/1.1\r\nHost: issue8261.com\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
362		&Request{
363			Method: "HEAD",
364			URL: &url.URL{
365				Path: "/",
366			},
367			Header: Header{
368				"Connection":     []string{"close"},
369				"Content-Length": []string{"0"},
370			},
371			Host:       "issue8261.com",
372			Proto:      "HTTP/1.1",
373			ProtoMajor: 1,
374			ProtoMinor: 1,
375			Close:      true,
376			RequestURI: "/",
377		},
378
379		noBodyStr,
380		noTrailer,
381		noError,
382	},
383
384	// http2 client preface:
385	{
386		"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n",
387		&Request{
388			Method: "PRI",
389			URL: &url.URL{
390				Path: "*",
391			},
392			Header:        Header{},
393			Proto:         "HTTP/2.0",
394			ProtoMajor:    2,
395			ProtoMinor:    0,
396			RequestURI:    "*",
397			ContentLength: -1,
398			Close:         true,
399		},
400		noBodyStr,
401		noTrailer,
402		noError,
403	},
404}
405
406func TestReadRequest(t *testing.T) {
407	for i := range reqTests {
408		tt := &reqTests[i]
409		req, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.Raw)))
410		if err != nil {
411			if err.Error() != tt.Error {
412				t.Errorf("#%d: error %q, want error %q", i, err.Error(), tt.Error)
413			}
414			continue
415		}
416		rbody := req.Body
417		req.Body = nil
418		testName := fmt.Sprintf("Test %d (%q)", i, tt.Raw)
419		diff(t, testName, req, tt.Req)
420		var bout bytes.Buffer
421		if rbody != nil {
422			_, err := io.Copy(&bout, rbody)
423			if err != nil {
424				t.Fatalf("%s: copying body: %v", testName, err)
425			}
426			rbody.Close()
427		}
428		body := bout.String()
429		if body != tt.Body {
430			t.Errorf("%s: Body = %q want %q", testName, body, tt.Body)
431		}
432		if !reflect.DeepEqual(tt.Trailer, req.Trailer) {
433			t.Errorf("%s: Trailers differ.\n got: %v\nwant: %v", testName, req.Trailer, tt.Trailer)
434		}
435	}
436}
437
438// reqBytes treats req as a request (with \n delimiters) and returns it with \r\n delimiters,
439// ending in \r\n\r\n
440func reqBytes(req string) []byte {
441	return []byte(strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n")
442}
443
444var badRequestTests = []struct {
445	name string
446	req  []byte
447}{
448	{"bad_connect_host", reqBytes("CONNECT []%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a HTTP/1.0")},
449	{"smuggle_two_contentlen", reqBytes(`POST / HTTP/1.1
450Content-Length: 3
451Content-Length: 4
452
453abc`)},
454	{"smuggle_content_len_head", reqBytes(`HEAD / HTTP/1.1
455Host: foo
456Content-Length: 5`)},
457
458	// golang.org/issue/22464
459	{"leading_space_in_header", reqBytes(`HEAD / HTTP/1.1
460 Host: foo
461Content-Length: 5`)},
462	{"leading_tab_in_header", reqBytes(`HEAD / HTTP/1.1
463\tHost: foo
464Content-Length: 5`)},
465}
466
467func TestReadRequest_Bad(t *testing.T) {
468	for _, tt := range badRequestTests {
469		got, err := ReadRequest(bufio.NewReader(bytes.NewReader(tt.req)))
470		if err == nil {
471			all, err := ioutil.ReadAll(got.Body)
472			t.Errorf("%s: got unexpected request = %#v\n  Body = %q, %v", tt.name, got, all, err)
473		}
474	}
475}
476