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