Home | History | Annotate | Download | only in http
      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 
      5 package http
      6 
      7 import (
      8 	"bufio"
      9 	"bytes"
     10 	"fmt"
     11 	"io"
     12 	"io/ioutil"
     13 	"net/url"
     14 	"reflect"
     15 	"strings"
     16 	"testing"
     17 )
     18 
     19 type reqTest struct {
     20 	Raw     string
     21 	Req     *Request
     22 	Body    string
     23 	Trailer Header
     24 	Error   string
     25 }
     26 
     27 var noError = ""
     28 var noBodyStr = ""
     29 var noTrailer Header = nil
     30 
     31 var 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 
    406 func 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
    440 func reqBytes(req string) []byte {
    441 	return []byte(strings.Replace(strings.TrimSpace(req), "\n", "\r\n", -1) + "\r\n\r\n")
    442 }
    443 
    444 var 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
    450 Content-Length: 3
    451 Content-Length: 4
    452 
    453 abc`)},
    454 	{"smuggle_content_len_head", reqBytes(`HEAD / HTTP/1.1
    455 Host: foo
    456 Content-Length: 5`)},
    457 }
    458 
    459 func TestReadRequest_Bad(t *testing.T) {
    460 	for _, tt := range badRequestTests {
    461 		got, err := ReadRequest(bufio.NewReader(bytes.NewReader(tt.req)))
    462 		if err == nil {
    463 			all, err := ioutil.ReadAll(got.Body)
    464 			t.Errorf("%s: got unexpected request = %#v\n  Body = %q, %v", tt.name, got, all, err)
    465 		}
    466 	}
    467 }
    468