Source file src/net/http/readrequest_test.go

     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  	"net/url"
    13  	"reflect"
    14  	"strings"
    15  	"testing"
    16  )
    17  
    18  type reqTest struct {
    19  	Raw     string
    20  	Req     *Request
    21  	Body    string
    22  	Trailer Header
    23  	Error   string
    24  }
    25  
    26  var noError = ""
    27  var noBodyStr = ""
    28  var noTrailer Header = nil
    29  
    30  var 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  
   405  func 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 strings.Builder
   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
   439  func reqBytes(req string) []byte {
   440  	return []byte(strings.ReplaceAll(strings.TrimSpace(req), "\n", "\r\n") + "\r\n\r\n")
   441  }
   442  
   443  var 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
   449  Content-Length: 3
   450  Content-Length: 4
   451  
   452  abc`)},
   453  	{"smuggle_two_content_len_head", reqBytes(`HEAD / HTTP/1.1
   454  Host: foo
   455  Content-Length: 4
   456  Content-Length: 5
   457  
   458  1234`)},
   459  
   460  	// golang.org/issue/22464
   461  	{"leading_space_in_header", reqBytes(`GET / HTTP/1.1
   462   Host: foo`)},
   463  	{"leading_tab_in_header", reqBytes(`GET / HTTP/1.1
   464  ` + "\t" + `Host: foo`)},
   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 := io.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