...
Run Format

Source file src/net/http/readrequest_test.go

Documentation: net/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  	// golang.org/issue/22464
   459  	{"leading_space_in_header", reqBytes(`HEAD / HTTP/1.1
   460   Host: foo
   461  Content-Length: 5`)},
   462  	{"leading_tab_in_header", reqBytes(`HEAD / HTTP/1.1
   463  \tHost: foo
   464  Content-Length: 5`)},
   465  }
   466  
   467  func 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  

View as plain text