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  	// 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  
   421  func 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
   455  func reqBytes(req string) []byte {
   456  	return []byte(strings.ReplaceAll(strings.TrimSpace(req), "\n", "\r\n") + "\r\n\r\n")
   457  }
   458  
   459  var 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
   465  Content-Length: 3
   466  Content-Length: 4
   467  
   468  abc`)},
   469  	{"smuggle_two_content_len_head", reqBytes(`HEAD / HTTP/1.1
   470  Host: foo
   471  Content-Length: 4
   472  Content-Length: 5
   473  
   474  1234`)},
   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  
   483  func 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  

View as plain text