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	// CONNECT request with domain name:
211	{
212		"CONNECT www.google.com:443 HTTP/1.1\r\n\r\n",
213
214		&Request{
215			Method: "CONNECT",
216			URL: &url.URL{
217				Host: "www.google.com:443",
218			},
219			Proto:         "HTTP/1.1",
220			ProtoMajor:    1,
221			ProtoMinor:    1,
222			Header:        Header{},
223			Close:         false,
224			ContentLength: 0,
225			Host:          "www.google.com:443",
226			RequestURI:    "www.google.com:443",
227		},
228
229		noBodyStr,
230		noTrailer,
231		noError,
232	},
233
234	// CONNECT request with IP address:
235	{
236		"CONNECT 127.0.0.1:6060 HTTP/1.1\r\n\r\n",
237
238		&Request{
239			Method: "CONNECT",
240			URL: &url.URL{
241				Host: "127.0.0.1:6060",
242			},
243			Proto:         "HTTP/1.1",
244			ProtoMajor:    1,
245			ProtoMinor:    1,
246			Header:        Header{},
247			Close:         false,
248			ContentLength: 0,
249			Host:          "127.0.0.1:6060",
250			RequestURI:    "127.0.0.1:6060",
251		},
252
253		noBodyStr,
254		noTrailer,
255		noError,
256	},
257
258	// CONNECT request for RPC:
259	{
260		"CONNECT /_goRPC_ HTTP/1.1\r\n\r\n",
261
262		&Request{
263			Method: "CONNECT",
264			URL: &url.URL{
265				Path: "/_goRPC_",
266			},
267			Proto:         "HTTP/1.1",
268			ProtoMajor:    1,
269			ProtoMinor:    1,
270			Header:        Header{},
271			Close:         false,
272			ContentLength: 0,
273			Host:          "",
274			RequestURI:    "/_goRPC_",
275		},
276
277		noBodyStr,
278		noTrailer,
279		noError,
280	},
281
282	// SSDP Notify request. golang.org/issue/3692
283	{
284		"NOTIFY * HTTP/1.1\r\nServer: foo\r\n\r\n",
285		&Request{
286			Method: "NOTIFY",
287			URL: &url.URL{
288				Path: "*",
289			},
290			Proto:      "HTTP/1.1",
291			ProtoMajor: 1,
292			ProtoMinor: 1,
293			Header: Header{
294				"Server": []string{"foo"},
295			},
296			Close:         false,
297			ContentLength: 0,
298			RequestURI:    "*",
299		},
300
301		noBodyStr,
302		noTrailer,
303		noError,
304	},
305
306	// OPTIONS request. Similar to golang.org/issue/3692
307	{
308		"OPTIONS * HTTP/1.1\r\nServer: foo\r\n\r\n",
309		&Request{
310			Method: "OPTIONS",
311			URL: &url.URL{
312				Path: "*",
313			},
314			Proto:      "HTTP/1.1",
315			ProtoMajor: 1,
316			ProtoMinor: 1,
317			Header: Header{
318				"Server": []string{"foo"},
319			},
320			Close:         false,
321			ContentLength: 0,
322			RequestURI:    "*",
323		},
324
325		noBodyStr,
326		noTrailer,
327		noError,
328	},
329
330	// Connection: close. golang.org/issue/8261
331	{
332		"GET / HTTP/1.1\r\nHost: issue8261.com\r\nConnection: close\r\n\r\n",
333		&Request{
334			Method: "GET",
335			URL: &url.URL{
336				Path: "/",
337			},
338			Header: Header{
339				// This wasn't removed from Go 1.0 to
340				// Go 1.3, so locking it in that we
341				// keep this:
342				"Connection": []string{"close"},
343			},
344			Host:       "issue8261.com",
345			Proto:      "HTTP/1.1",
346			ProtoMajor: 1,
347			ProtoMinor: 1,
348			Close:      true,
349			RequestURI: "/",
350		},
351
352		noBodyStr,
353		noTrailer,
354		noError,
355	},
356
357	// HEAD with Content-Length 0. Make sure this is permitted,
358	// since I think we used to send it.
359	{
360		"HEAD / HTTP/1.1\r\nHost: issue8261.com\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
361		&Request{
362			Method: "HEAD",
363			URL: &url.URL{
364				Path: "/",
365			},
366			Header: Header{
367				"Connection":     []string{"close"},
368				"Content-Length": []string{"0"},
369			},
370			Host:       "issue8261.com",
371			Proto:      "HTTP/1.1",
372			ProtoMajor: 1,
373			ProtoMinor: 1,
374			Close:      true,
375			RequestURI: "/",
376		},
377
378		noBodyStr,
379		noTrailer,
380		noError,
381	},
382
383	// http2 client preface:
384	{
385		"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n",
386		&Request{
387			Method: "PRI",
388			URL: &url.URL{
389				Path: "*",
390			},
391			Header:        Header{},
392			Proto:         "HTTP/2.0",
393			ProtoMajor:    2,
394			ProtoMinor:    0,
395			RequestURI:    "*",
396			ContentLength: -1,
397			Close:         true,
398		},
399		noBodyStr,
400		noTrailer,
401		noError,
402	},
403}
404
405func TestReadRequest(t *testing.T) {
406	for i := range reqTests {
407		tt := &reqTests[i]
408		req, err := ReadRequest(bufio.NewReader(strings.NewReader(tt.Raw)))
409		if err != nil {
410			if err.Error() != tt.Error {
411				t.Errorf("#%d: error %q, want error %q", i, err.Error(), tt.Error)
412			}
413			continue
414		}
415		rbody := req.Body
416		req.Body = nil
417		testName := fmt.Sprintf("Test %d (%q)", i, tt.Raw)
418		diff(t, testName, req, tt.Req)
419		var bout bytes.Buffer
420		if rbody != nil {
421			_, err := io.Copy(&bout, rbody)
422			if err != nil {
423				t.Fatalf("%s: copying body: %v", testName, err)
424			}
425			rbody.Close()
426		}
427		body := bout.String()
428		if body != tt.Body {
429			t.Errorf("%s: Body = %q want %q", testName, body, tt.Body)
430		}
431		if !reflect.DeepEqual(tt.Trailer, req.Trailer) {
432			t.Errorf("%s: Trailers differ.\n got: %v\nwant: %v", testName, req.Trailer, tt.Trailer)
433		}
434	}
435}
436
437// reqBytes treats req as a request (with \n delimiters) and returns it with \r\n delimiters,
438// ending in \r\n\r\n
439func reqBytes(req string) []byte {
440	return []byte(strings.ReplaceAll(strings.TrimSpace(req), "\n", "\r\n") + "\r\n\r\n")
441}
442
443var badRequestTests = []struct {
444	name string
445	req  []byte
446}{
447	{"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")},
448	{"smuggle_two_contentlen", reqBytes(`POST / HTTP/1.1
449Content-Length: 3
450Content-Length: 4
451
452abc`)},
453	{"smuggle_content_len_head", reqBytes(`HEAD / HTTP/1.1
454Host: foo
455Content-Length: 5`)},
456
457	// golang.org/issue/22464
458	{"leading_space_in_header", reqBytes(`HEAD / HTTP/1.1
459 Host: foo
460Content-Length: 5`)},
461	{"leading_tab_in_header", reqBytes(`HEAD / HTTP/1.1
462\tHost: foo
463Content-Length: 5`)},
464}
465
466func TestReadRequest_Bad(t *testing.T) {
467	for _, tt := range badRequestTests {
468		got, err := ReadRequest(bufio.NewReader(bytes.NewReader(tt.req)))
469		if err == nil {
470			all, err := io.ReadAll(got.Body)
471			t.Errorf("%s: got unexpected request = %#v\n  Body = %q, %v", tt.name, got, all, err)
472		}
473	}
474}
475