Source file src/net/http/requestwrite_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  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net"
    14  	"net/url"
    15  	"strings"
    16  	"testing"
    17  	"testing/iotest"
    18  	"testing/synctest"
    19  	"time"
    20  )
    21  
    22  type reqWriteTest struct {
    23  	Req  Request
    24  	Body any // optional []byte or func() io.ReadCloser to populate Req.Body
    25  
    26  	// Any of these three may be empty to skip that test.
    27  	WantWrite string // Request.Write
    28  	WantProxy string // Request.WriteProxy
    29  
    30  	WantError error // wanted error from Request.Write
    31  }
    32  
    33  var reqWriteTests = []reqWriteTest{
    34  	// HTTP/1.1 => chunked coding; no body; no trailer
    35  	0: {
    36  		Req: Request{
    37  			Method: "GET",
    38  			URL: &url.URL{
    39  				Scheme: "http",
    40  				Host:   "www.techcrunch.com",
    41  				Path:   "/",
    42  			},
    43  			Proto:      "HTTP/1.1",
    44  			ProtoMajor: 1,
    45  			ProtoMinor: 1,
    46  			Header: Header{
    47  				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
    48  				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
    49  				"Accept-Encoding":  {"gzip,deflate"},
    50  				"Accept-Language":  {"en-us,en;q=0.5"},
    51  				"Keep-Alive":       {"300"},
    52  				"Proxy-Connection": {"keep-alive"},
    53  				"User-Agent":       {"Fake"},
    54  			},
    55  			Body:  nil,
    56  			Close: false,
    57  			Host:  "www.techcrunch.com",
    58  			Form:  map[string][]string{},
    59  		},
    60  
    61  		WantWrite: "GET / HTTP/1.1\r\n" +
    62  			"Host: www.techcrunch.com\r\n" +
    63  			"User-Agent: Fake\r\n" +
    64  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    65  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    66  			"Accept-Encoding: gzip,deflate\r\n" +
    67  			"Accept-Language: en-us,en;q=0.5\r\n" +
    68  			"Keep-Alive: 300\r\n" +
    69  			"Proxy-Connection: keep-alive\r\n\r\n",
    70  
    71  		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
    72  			"Host: www.techcrunch.com\r\n" +
    73  			"User-Agent: Fake\r\n" +
    74  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    75  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    76  			"Accept-Encoding: gzip,deflate\r\n" +
    77  			"Accept-Language: en-us,en;q=0.5\r\n" +
    78  			"Keep-Alive: 300\r\n" +
    79  			"Proxy-Connection: keep-alive\r\n\r\n",
    80  	},
    81  	// HTTP/1.1 => chunked coding; body; empty trailer
    82  	1: {
    83  		Req: Request{
    84  			Method: "GET",
    85  			URL: &url.URL{
    86  				Scheme: "http",
    87  				Host:   "www.google.com",
    88  				Path:   "/search",
    89  			},
    90  			ProtoMajor:       1,
    91  			ProtoMinor:       1,
    92  			Header:           Header{},
    93  			TransferEncoding: []string{"chunked"},
    94  		},
    95  
    96  		Body: []byte("abcdef"),
    97  
    98  		WantWrite: "GET /search HTTP/1.1\r\n" +
    99  			"Host: www.google.com\r\n" +
   100  			"User-Agent: Go-http-client/1.1\r\n" +
   101  			"Transfer-Encoding: chunked\r\n\r\n" +
   102  			chunk("abcdef") + chunk(""),
   103  
   104  		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
   105  			"Host: www.google.com\r\n" +
   106  			"User-Agent: Go-http-client/1.1\r\n" +
   107  			"Transfer-Encoding: chunked\r\n\r\n" +
   108  			chunk("abcdef") + chunk(""),
   109  	},
   110  	// HTTP/1.1 POST => chunked coding; body; empty trailer
   111  	2: {
   112  		Req: Request{
   113  			Method: "POST",
   114  			URL: &url.URL{
   115  				Scheme: "http",
   116  				Host:   "www.google.com",
   117  				Path:   "/search",
   118  			},
   119  			ProtoMajor:       1,
   120  			ProtoMinor:       1,
   121  			Header:           Header{},
   122  			Close:            true,
   123  			TransferEncoding: []string{"chunked"},
   124  		},
   125  
   126  		Body: []byte("abcdef"),
   127  
   128  		WantWrite: "POST /search HTTP/1.1\r\n" +
   129  			"Host: www.google.com\r\n" +
   130  			"User-Agent: Go-http-client/1.1\r\n" +
   131  			"Connection: close\r\n" +
   132  			"Transfer-Encoding: chunked\r\n\r\n" +
   133  			chunk("abcdef") + chunk(""),
   134  
   135  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   136  			"Host: www.google.com\r\n" +
   137  			"User-Agent: Go-http-client/1.1\r\n" +
   138  			"Connection: close\r\n" +
   139  			"Transfer-Encoding: chunked\r\n\r\n" +
   140  			chunk("abcdef") + chunk(""),
   141  	},
   142  
   143  	// HTTP/1.1 POST with Content-Length, no chunking
   144  	3: {
   145  		Req: Request{
   146  			Method: "POST",
   147  			URL: &url.URL{
   148  				Scheme: "http",
   149  				Host:   "www.google.com",
   150  				Path:   "/search",
   151  			},
   152  			ProtoMajor:    1,
   153  			ProtoMinor:    1,
   154  			Header:        Header{},
   155  			Close:         true,
   156  			ContentLength: 6,
   157  		},
   158  
   159  		Body: []byte("abcdef"),
   160  
   161  		WantWrite: "POST /search HTTP/1.1\r\n" +
   162  			"Host: www.google.com\r\n" +
   163  			"User-Agent: Go-http-client/1.1\r\n" +
   164  			"Connection: close\r\n" +
   165  			"Content-Length: 6\r\n" +
   166  			"\r\n" +
   167  			"abcdef",
   168  
   169  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   170  			"Host: www.google.com\r\n" +
   171  			"User-Agent: Go-http-client/1.1\r\n" +
   172  			"Connection: close\r\n" +
   173  			"Content-Length: 6\r\n" +
   174  			"\r\n" +
   175  			"abcdef",
   176  	},
   177  
   178  	// HTTP/1.1 POST with Content-Length in headers
   179  	4: {
   180  		Req: Request{
   181  			Method: "POST",
   182  			URL:    mustParseURL("http://example.com/"),
   183  			Host:   "example.com",
   184  			Header: Header{
   185  				"Content-Length": []string{"10"}, // ignored
   186  			},
   187  			ContentLength: 6,
   188  		},
   189  
   190  		Body: []byte("abcdef"),
   191  
   192  		WantWrite: "POST / HTTP/1.1\r\n" +
   193  			"Host: example.com\r\n" +
   194  			"User-Agent: Go-http-client/1.1\r\n" +
   195  			"Content-Length: 6\r\n" +
   196  			"\r\n" +
   197  			"abcdef",
   198  
   199  		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
   200  			"Host: example.com\r\n" +
   201  			"User-Agent: Go-http-client/1.1\r\n" +
   202  			"Content-Length: 6\r\n" +
   203  			"\r\n" +
   204  			"abcdef",
   205  	},
   206  
   207  	// default to HTTP/1.1
   208  	5: {
   209  		Req: Request{
   210  			Method: "GET",
   211  			URL:    mustParseURL("/search"),
   212  			Host:   "www.google.com",
   213  		},
   214  
   215  		WantWrite: "GET /search HTTP/1.1\r\n" +
   216  			"Host: www.google.com\r\n" +
   217  			"User-Agent: Go-http-client/1.1\r\n" +
   218  			"\r\n",
   219  	},
   220  
   221  	// Request with a 0 ContentLength and a 0 byte body.
   222  	6: {
   223  		Req: Request{
   224  			Method:        "POST",
   225  			URL:           mustParseURL("/"),
   226  			Host:          "example.com",
   227  			ProtoMajor:    1,
   228  			ProtoMinor:    1,
   229  			ContentLength: 0, // as if unset by user
   230  		},
   231  
   232  		Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
   233  
   234  		WantWrite: "POST / HTTP/1.1\r\n" +
   235  			"Host: example.com\r\n" +
   236  			"User-Agent: Go-http-client/1.1\r\n" +
   237  			"Transfer-Encoding: chunked\r\n" +
   238  			"\r\n0\r\n\r\n",
   239  
   240  		WantProxy: "POST / HTTP/1.1\r\n" +
   241  			"Host: example.com\r\n" +
   242  			"User-Agent: Go-http-client/1.1\r\n" +
   243  			"Transfer-Encoding: chunked\r\n" +
   244  			"\r\n0\r\n\r\n",
   245  	},
   246  
   247  	// Request with a 0 ContentLength and a nil body.
   248  	7: {
   249  		Req: Request{
   250  			Method:        "POST",
   251  			URL:           mustParseURL("/"),
   252  			Host:          "example.com",
   253  			ProtoMajor:    1,
   254  			ProtoMinor:    1,
   255  			ContentLength: 0, // as if unset by user
   256  		},
   257  
   258  		Body: func() io.ReadCloser { return nil },
   259  
   260  		WantWrite: "POST / HTTP/1.1\r\n" +
   261  			"Host: example.com\r\n" +
   262  			"User-Agent: Go-http-client/1.1\r\n" +
   263  			"Content-Length: 0\r\n" +
   264  			"\r\n",
   265  
   266  		WantProxy: "POST / HTTP/1.1\r\n" +
   267  			"Host: example.com\r\n" +
   268  			"User-Agent: Go-http-client/1.1\r\n" +
   269  			"Content-Length: 0\r\n" +
   270  			"\r\n",
   271  	},
   272  
   273  	// Request with a 0 ContentLength and a 1 byte body.
   274  	8: {
   275  		Req: Request{
   276  			Method:        "POST",
   277  			URL:           mustParseURL("/"),
   278  			Host:          "example.com",
   279  			ProtoMajor:    1,
   280  			ProtoMinor:    1,
   281  			ContentLength: 0, // as if unset by user
   282  		},
   283  
   284  		Body: func() io.ReadCloser { return io.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
   285  
   286  		WantWrite: "POST / HTTP/1.1\r\n" +
   287  			"Host: example.com\r\n" +
   288  			"User-Agent: Go-http-client/1.1\r\n" +
   289  			"Transfer-Encoding: chunked\r\n\r\n" +
   290  			chunk("x") + chunk(""),
   291  
   292  		WantProxy: "POST / HTTP/1.1\r\n" +
   293  			"Host: example.com\r\n" +
   294  			"User-Agent: Go-http-client/1.1\r\n" +
   295  			"Transfer-Encoding: chunked\r\n\r\n" +
   296  			chunk("x") + chunk(""),
   297  	},
   298  
   299  	// Request with a ContentLength of 10 but a 5 byte body.
   300  	9: {
   301  		Req: Request{
   302  			Method:        "POST",
   303  			URL:           mustParseURL("/"),
   304  			Host:          "example.com",
   305  			ProtoMajor:    1,
   306  			ProtoMinor:    1,
   307  			ContentLength: 10, // but we're going to send only 5 bytes
   308  		},
   309  		Body:      []byte("12345"),
   310  		WantError: errors.New("http: ContentLength=10 with Body length 5"),
   311  	},
   312  
   313  	// Request with a ContentLength of 4 but an 8 byte body.
   314  	10: {
   315  		Req: Request{
   316  			Method:        "POST",
   317  			URL:           mustParseURL("/"),
   318  			Host:          "example.com",
   319  			ProtoMajor:    1,
   320  			ProtoMinor:    1,
   321  			ContentLength: 4, // but we're going to try to send 8 bytes
   322  		},
   323  		Body:      []byte("12345678"),
   324  		WantError: errors.New("http: ContentLength=4 with Body length 8"),
   325  	},
   326  
   327  	// Request with a 5 ContentLength and nil body.
   328  	11: {
   329  		Req: Request{
   330  			Method:        "POST",
   331  			URL:           mustParseURL("/"),
   332  			Host:          "example.com",
   333  			ProtoMajor:    1,
   334  			ProtoMinor:    1,
   335  			ContentLength: 5, // but we'll omit the body
   336  		},
   337  		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
   338  	},
   339  
   340  	// Request with a 0 ContentLength and a body with 1 byte content and an error.
   341  	12: {
   342  		Req: Request{
   343  			Method:        "POST",
   344  			URL:           mustParseURL("/"),
   345  			Host:          "example.com",
   346  			ProtoMajor:    1,
   347  			ProtoMinor:    1,
   348  			ContentLength: 0, // as if unset by user
   349  		},
   350  
   351  		Body: func() io.ReadCloser {
   352  			err := errors.New("Custom reader error")
   353  			errReader := iotest.ErrReader(err)
   354  			return io.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
   355  		},
   356  
   357  		WantError: errors.New("Custom reader error"),
   358  	},
   359  
   360  	// Request with a 0 ContentLength and a body without content and an error.
   361  	13: {
   362  		Req: Request{
   363  			Method:        "POST",
   364  			URL:           mustParseURL("/"),
   365  			Host:          "example.com",
   366  			ProtoMajor:    1,
   367  			ProtoMinor:    1,
   368  			ContentLength: 0, // as if unset by user
   369  		},
   370  
   371  		Body: func() io.ReadCloser {
   372  			err := errors.New("Custom reader error")
   373  			errReader := iotest.ErrReader(err)
   374  			return io.NopCloser(errReader)
   375  		},
   376  
   377  		WantError: errors.New("Custom reader error"),
   378  	},
   379  
   380  	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
   381  	// and doesn't add a User-Agent.
   382  	14: {
   383  		Req: Request{
   384  			Method:     "GET",
   385  			URL:        mustParseURL("/foo"),
   386  			ProtoMajor: 1,
   387  			ProtoMinor: 0,
   388  			Header: Header{
   389  				"X-Foo": []string{"X-Bar"},
   390  			},
   391  		},
   392  
   393  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   394  			"Host: \r\n" +
   395  			"User-Agent: Go-http-client/1.1\r\n" +
   396  			"X-Foo: X-Bar\r\n\r\n",
   397  	},
   398  
   399  	// If no Request.Host and no Request.URL.Host, we send
   400  	// an empty Host header, and don't use
   401  	// Request.Header["Host"]. This is just testing that
   402  	// we don't change Go 1.0 behavior.
   403  	15: {
   404  		Req: Request{
   405  			Method: "GET",
   406  			Host:   "",
   407  			URL: &url.URL{
   408  				Scheme: "http",
   409  				Host:   "",
   410  				Path:   "/search",
   411  			},
   412  			ProtoMajor: 1,
   413  			ProtoMinor: 1,
   414  			Header: Header{
   415  				"Host": []string{"bad.example.com"},
   416  			},
   417  		},
   418  
   419  		WantWrite: "GET /search HTTP/1.1\r\n" +
   420  			"Host: \r\n" +
   421  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   422  	},
   423  
   424  	// Opaque test #1 from golang.org/issue/4860
   425  	16: {
   426  		Req: Request{
   427  			Method: "GET",
   428  			URL: &url.URL{
   429  				Scheme: "http",
   430  				Host:   "www.google.com",
   431  				Opaque: "/%2F/%2F/",
   432  			},
   433  			ProtoMajor: 1,
   434  			ProtoMinor: 1,
   435  			Header:     Header{},
   436  		},
   437  
   438  		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
   439  			"Host: www.google.com\r\n" +
   440  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   441  	},
   442  
   443  	// Opaque test #2 from golang.org/issue/4860
   444  	17: {
   445  		Req: Request{
   446  			Method: "GET",
   447  			URL: &url.URL{
   448  				Scheme: "http",
   449  				Host:   "x.google.com",
   450  				Opaque: "//y.google.com/%2F/%2F/",
   451  			},
   452  			ProtoMajor: 1,
   453  			ProtoMinor: 1,
   454  			Header:     Header{},
   455  		},
   456  
   457  		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
   458  			"Host: x.google.com\r\n" +
   459  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   460  	},
   461  
   462  	// Testing custom case in header keys. Issue 5022.
   463  	18: {
   464  		Req: Request{
   465  			Method: "GET",
   466  			URL: &url.URL{
   467  				Scheme: "http",
   468  				Host:   "www.google.com",
   469  				Path:   "/",
   470  			},
   471  			Proto:      "HTTP/1.1",
   472  			ProtoMajor: 1,
   473  			ProtoMinor: 1,
   474  			Header: Header{
   475  				"ALL-CAPS": {"x"},
   476  			},
   477  		},
   478  
   479  		WantWrite: "GET / HTTP/1.1\r\n" +
   480  			"Host: www.google.com\r\n" +
   481  			"User-Agent: Go-http-client/1.1\r\n" +
   482  			"ALL-CAPS: x\r\n" +
   483  			"\r\n",
   484  	},
   485  
   486  	// Request with host header field; IPv6 address with zone identifier
   487  	19: {
   488  		Req: Request{
   489  			Method: "GET",
   490  			URL: &url.URL{
   491  				Host: "[fe80::1%en0]",
   492  			},
   493  		},
   494  
   495  		WantWrite: "GET / HTTP/1.1\r\n" +
   496  			"Host: [fe80::1]\r\n" +
   497  			"User-Agent: Go-http-client/1.1\r\n" +
   498  			"\r\n",
   499  	},
   500  
   501  	// Request with optional host header field; IPv6 address with zone identifier
   502  	20: {
   503  		Req: Request{
   504  			Method: "GET",
   505  			URL: &url.URL{
   506  				Host: "www.example.com",
   507  			},
   508  			Host: "[fe80::1%en0]:8080",
   509  		},
   510  
   511  		WantWrite: "GET / HTTP/1.1\r\n" +
   512  			"Host: [fe80::1]:8080\r\n" +
   513  			"User-Agent: Go-http-client/1.1\r\n" +
   514  			"\r\n",
   515  	},
   516  
   517  	// CONNECT without Opaque
   518  	21: {
   519  		Req: Request{
   520  			Method: "CONNECT",
   521  			URL: &url.URL{
   522  				Scheme: "https", // of proxy.com
   523  				Host:   "proxy.com",
   524  			},
   525  		},
   526  		// What we used to do, locking that behavior in:
   527  		WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" +
   528  			"Host: proxy.com\r\n" +
   529  			"User-Agent: Go-http-client/1.1\r\n" +
   530  			"\r\n",
   531  	},
   532  
   533  	// CONNECT with Opaque
   534  	22: {
   535  		Req: Request{
   536  			Method: "CONNECT",
   537  			URL: &url.URL{
   538  				Scheme: "https", // of proxy.com
   539  				Host:   "proxy.com",
   540  				Opaque: "backend:443",
   541  			},
   542  		},
   543  		WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" +
   544  			"Host: proxy.com\r\n" +
   545  			"User-Agent: Go-http-client/1.1\r\n" +
   546  			"\r\n",
   547  	},
   548  
   549  	// Verify that a nil header value doesn't get written.
   550  	23: {
   551  		Req: Request{
   552  			Method: "GET",
   553  			URL:    mustParseURL("/foo"),
   554  			Header: Header{
   555  				"X-Foo":             []string{"X-Bar"},
   556  				"X-Idempotency-Key": nil,
   557  			},
   558  		},
   559  
   560  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   561  			"Host: \r\n" +
   562  			"User-Agent: Go-http-client/1.1\r\n" +
   563  			"X-Foo: X-Bar\r\n\r\n",
   564  	},
   565  	24: {
   566  		Req: Request{
   567  			Method: "GET",
   568  			URL:    mustParseURL("/foo"),
   569  			Header: Header{
   570  				"X-Foo":             []string{"X-Bar"},
   571  				"X-Idempotency-Key": []string{},
   572  			},
   573  		},
   574  
   575  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   576  			"Host: \r\n" +
   577  			"User-Agent: Go-http-client/1.1\r\n" +
   578  			"X-Foo: X-Bar\r\n\r\n",
   579  	},
   580  
   581  	25: {
   582  		Req: Request{
   583  			Method: "GET",
   584  			URL: &url.URL{
   585  				Host:     "www.example.com",
   586  				RawQuery: "new\nline", // or any CTL
   587  			},
   588  		},
   589  		WantError: errors.New("net/http: can't write control character in Request.URL"),
   590  	},
   591  
   592  	26: { // Request with nil body and PATCH method. Issue #40978
   593  		Req: Request{
   594  			Method:        "PATCH",
   595  			URL:           mustParseURL("/"),
   596  			Host:          "example.com",
   597  			ProtoMajor:    1,
   598  			ProtoMinor:    1,
   599  			ContentLength: 0, // as if unset by user
   600  		},
   601  		Body: nil,
   602  		WantWrite: "PATCH / HTTP/1.1\r\n" +
   603  			"Host: example.com\r\n" +
   604  			"User-Agent: Go-http-client/1.1\r\n" +
   605  			"Content-Length: 0\r\n\r\n",
   606  		WantProxy: "PATCH / HTTP/1.1\r\n" +
   607  			"Host: example.com\r\n" +
   608  			"User-Agent: Go-http-client/1.1\r\n" +
   609  			"Content-Length: 0\r\n\r\n",
   610  	},
   611  
   612  	// Valid Trailer keeps working after trailer validation. Issue #78775
   613  	27: {
   614  		Req: Request{
   615  			Method: "POST",
   616  			URL: &url.URL{
   617  				Scheme: "http",
   618  				Host:   "example.com",
   619  				Path:   "/",
   620  			},
   621  			ProtoMajor:       1,
   622  			ProtoMinor:       1,
   623  			Header:           Header{},
   624  			TransferEncoding: []string{"chunked"},
   625  			Trailer:          Header{"X-Trailer": {"ok"}},
   626  		},
   627  
   628  		Body: []byte("abcdef"),
   629  
   630  		WantWrite: "POST / HTTP/1.1\r\n" +
   631  			"Host: example.com\r\n" +
   632  			"User-Agent: Go-http-client/1.1\r\n" +
   633  			"Transfer-Encoding: chunked\r\n" +
   634  			"Trailer: X-Trailer\r\n\r\n" +
   635  			chunk("abcdef") +
   636  			"0\r\n" +
   637  			"X-Trailer: ok\r\n" +
   638  			"\r\n",
   639  	},
   640  
   641  	// Trailer names with control characters must not reach the wire,
   642  	// where they would permit header injection on the "Trailer:" line.
   643  	// Issue #78775
   644  	28: {
   645  		Req: Request{
   646  			Method: "POST",
   647  			URL: &url.URL{
   648  				Scheme: "http",
   649  				Host:   "example.com",
   650  				Path:   "/",
   651  			},
   652  			ProtoMajor:       1,
   653  			ProtoMinor:       1,
   654  			Header:           Header{},
   655  			TransferEncoding: []string{"chunked"},
   656  			Trailer:          Header{"X-Trailer\r\nInjected: 1": {"ok"}},
   657  		},
   658  
   659  		Body: []byte("abcdef"),
   660  
   661  		WantError: errors.New(`net/http: invalid trailer field name "X-Trailer\r\nInjected: 1"`),
   662  	},
   663  
   664  	// Trailer values with control characters are rejected as well. Issue #78775
   665  	29: {
   666  		Req: Request{
   667  			Method: "POST",
   668  			URL: &url.URL{
   669  				Scheme: "http",
   670  				Host:   "example.com",
   671  				Path:   "/",
   672  			},
   673  			ProtoMajor:       1,
   674  			ProtoMinor:       1,
   675  			Header:           Header{},
   676  			TransferEncoding: []string{"chunked"},
   677  			Trailer:          Header{"X-Trailer": {"evil\r\nInjected: 1"}},
   678  		},
   679  
   680  		Body: []byte("abcdef"),
   681  
   682  		WantError: errors.New(`net/http: invalid trailer field value for "X-Trailer"`),
   683  	},
   684  
   685  	// An empty Trailer name is rejected. Issue #78775
   686  	30: {
   687  		Req: Request{
   688  			Method: "POST",
   689  			URL: &url.URL{
   690  				Scheme: "http",
   691  				Host:   "example.com",
   692  				Path:   "/",
   693  			},
   694  			ProtoMajor:       1,
   695  			ProtoMinor:       1,
   696  			Header:           Header{},
   697  			TransferEncoding: []string{"chunked"},
   698  			Trailer:          Header{"": {"ok"}},
   699  		},
   700  
   701  		Body: []byte("abcdef"),
   702  
   703  		WantError: errors.New(`net/http: invalid trailer field name ""`),
   704  	},
   705  
   706  	// A later (non-first) value in a Trailer is validated too. Issue #78775
   707  	31: {
   708  		Req: Request{
   709  			Method: "POST",
   710  			URL: &url.URL{
   711  				Scheme: "http",
   712  				Host:   "example.com",
   713  				Path:   "/",
   714  			},
   715  			ProtoMajor:       1,
   716  			ProtoMinor:       1,
   717  			Header:           Header{},
   718  			TransferEncoding: []string{"chunked"},
   719  			Trailer:          Header{"X-Trailer": {"ok", "evil\r\nInjected: 1"}},
   720  		},
   721  
   722  		Body: []byte("abcdef"),
   723  
   724  		WantError: errors.New(`net/http: invalid trailer field value for "X-Trailer"`),
   725  	},
   726  }
   727  
   728  func TestRequestWrite(t *testing.T) {
   729  	for i := range reqWriteTests {
   730  		tt := &reqWriteTests[i]
   731  
   732  		setBody := func() {
   733  			if tt.Body == nil {
   734  				return
   735  			}
   736  			switch b := tt.Body.(type) {
   737  			case []byte:
   738  				tt.Req.Body = io.NopCloser(bytes.NewReader(b))
   739  			case func() io.ReadCloser:
   740  				tt.Req.Body = b()
   741  			}
   742  		}
   743  		setBody()
   744  		if tt.Req.Header == nil {
   745  			tt.Req.Header = make(Header)
   746  		}
   747  
   748  		var braw strings.Builder
   749  		err := tt.Req.Write(&braw)
   750  		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
   751  			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
   752  			continue
   753  		}
   754  		if err != nil {
   755  			continue
   756  		}
   757  
   758  		if tt.WantWrite != "" {
   759  			sraw := braw.String()
   760  			if sraw != tt.WantWrite {
   761  				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
   762  				continue
   763  			}
   764  		}
   765  
   766  		if tt.WantProxy != "" {
   767  			setBody()
   768  			var praw strings.Builder
   769  			err = tt.Req.WriteProxy(&praw)
   770  			if err != nil {
   771  				t.Errorf("WriteProxy #%d: %s", i, err)
   772  				continue
   773  			}
   774  			sraw := praw.String()
   775  			if sraw != tt.WantProxy {
   776  				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
   777  				continue
   778  			}
   779  		}
   780  	}
   781  }
   782  
   783  func TestRequestWriteTransport(t *testing.T) {
   784  	t.Parallel()
   785  
   786  	// Run this test in a synctest bubble, since it relies on the transport
   787  	// successfully probing the request body within 200ms
   788  	// (see transferWriter.probeRequestBody).
   789  	// This occasionally flakes on slow builders (#52575) if we don't use a fake clock.
   790  	synctest.Test(t, testRequestWriteTransport)
   791  }
   792  func testRequestWriteTransport(t *testing.T) {
   793  	matchSubstr := func(substr string) func(string) error {
   794  		return func(written string) error {
   795  			if !strings.Contains(written, substr) {
   796  				return fmt.Errorf("expected substring %q in request: %s", substr, written)
   797  			}
   798  			return nil
   799  		}
   800  	}
   801  
   802  	noContentLengthOrTransferEncoding := func(req string) error {
   803  		if strings.Contains(req, "Content-Length: ") {
   804  			return fmt.Errorf("unexpected Content-Length in request: %s", req)
   805  		}
   806  		if strings.Contains(req, "Transfer-Encoding: ") {
   807  			return fmt.Errorf("unexpected Transfer-Encoding in request: %s", req)
   808  		}
   809  		return nil
   810  	}
   811  
   812  	all := func(checks ...func(string) error) func(string) error {
   813  		return func(req string) error {
   814  			for _, c := range checks {
   815  				if err := c(req); err != nil {
   816  					return err
   817  				}
   818  			}
   819  			return nil
   820  		}
   821  	}
   822  
   823  	type testCase struct {
   824  		method string
   825  		clen   int64 // ContentLength
   826  		body   io.ReadCloser
   827  		want   func(string) error
   828  
   829  		// optional:
   830  		init         func(*testCase)
   831  		afterReqRead func()
   832  	}
   833  
   834  	tests := []testCase{
   835  		{
   836  			method: "GET",
   837  			want:   noContentLengthOrTransferEncoding,
   838  		},
   839  		{
   840  			method: "GET",
   841  			body:   io.NopCloser(strings.NewReader("")),
   842  			want:   noContentLengthOrTransferEncoding,
   843  		},
   844  		{
   845  			method: "GET",
   846  			clen:   -1,
   847  			body:   io.NopCloser(strings.NewReader("")),
   848  			want:   noContentLengthOrTransferEncoding,
   849  		},
   850  		// A GET with a body, with explicit content length:
   851  		{
   852  			method: "GET",
   853  			clen:   7,
   854  			body:   io.NopCloser(strings.NewReader("foobody")),
   855  			want: all(matchSubstr("Content-Length: 7"),
   856  				matchSubstr("foobody")),
   857  		},
   858  		// A GET with a body, sniffing the leading "f" from "foobody".
   859  		{
   860  			method: "GET",
   861  			clen:   -1,
   862  			body:   io.NopCloser(strings.NewReader("foobody")),
   863  			want: all(matchSubstr("Transfer-Encoding: chunked"),
   864  				matchSubstr("\r\n1\r\nf\r\n"),
   865  				matchSubstr("oobody")),
   866  		},
   867  		// But a POST request is expected to have a body, so
   868  		// no sniffing happens:
   869  		{
   870  			method: "POST",
   871  			clen:   -1,
   872  			body:   io.NopCloser(strings.NewReader("foobody")),
   873  			want: all(matchSubstr("Transfer-Encoding: chunked"),
   874  				matchSubstr("foobody")),
   875  		},
   876  		{
   877  			method: "POST",
   878  			clen:   -1,
   879  			body:   io.NopCloser(strings.NewReader("")),
   880  			want:   all(matchSubstr("Transfer-Encoding: chunked")),
   881  		},
   882  		// Verify that a blocking Request.Body doesn't block forever.
   883  		{
   884  			method: "GET",
   885  			clen:   -1,
   886  			init: func(tt *testCase) {
   887  				pr, pw := io.Pipe()
   888  				tt.afterReqRead = func() {
   889  					pw.Close()
   890  				}
   891  				tt.body = io.NopCloser(pr)
   892  			},
   893  			want: matchSubstr("Transfer-Encoding: chunked"),
   894  		},
   895  	}
   896  
   897  	for i, tt := range tests {
   898  		if tt.init != nil {
   899  			tt.init(&tt)
   900  		}
   901  		req := &Request{
   902  			Method: tt.method,
   903  			URL: &url.URL{
   904  				Scheme: "http",
   905  				Host:   "example.com",
   906  			},
   907  			Header:        make(Header),
   908  			ContentLength: tt.clen,
   909  			Body:          tt.body,
   910  		}
   911  		got, err := dumpRequestOut(req, tt.afterReqRead)
   912  		if err != nil {
   913  			t.Errorf("test[%d]: %v", i, err)
   914  			continue
   915  		}
   916  		if err := tt.want(string(got)); err != nil {
   917  			t.Errorf("test[%d]: %v", i, err)
   918  		}
   919  	}
   920  }
   921  
   922  type closeChecker struct {
   923  	io.Reader
   924  	closed bool
   925  }
   926  
   927  func (rc *closeChecker) Close() error {
   928  	rc.closed = true
   929  	return nil
   930  }
   931  
   932  // TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
   933  // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
   934  // inside a NopCloser, and that it serializes it correctly.
   935  func TestRequestWriteClosesBody(t *testing.T) {
   936  	rc := &closeChecker{Reader: strings.NewReader("my body")}
   937  	req, err := NewRequest("POST", "http://foo.com/", rc)
   938  	if err != nil {
   939  		t.Fatal(err)
   940  	}
   941  	buf := new(strings.Builder)
   942  	if err := req.Write(buf); err != nil {
   943  		t.Error(err)
   944  	}
   945  	if !rc.closed {
   946  		t.Error("body not closed after write")
   947  	}
   948  	expected := "POST / HTTP/1.1\r\n" +
   949  		"Host: foo.com\r\n" +
   950  		"User-Agent: Go-http-client/1.1\r\n" +
   951  		"Transfer-Encoding: chunked\r\n\r\n" +
   952  		chunk("my body") +
   953  		chunk("")
   954  	if buf.String() != expected {
   955  		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
   956  	}
   957  }
   958  
   959  func chunk(s string) string {
   960  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   961  }
   962  
   963  func mustParseURL(s string) *url.URL {
   964  	u, err := url.Parse(s)
   965  	if err != nil {
   966  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   967  	}
   968  	return u
   969  }
   970  
   971  type writerFunc func([]byte) (int, error)
   972  
   973  func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
   974  
   975  // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
   976  func TestRequestWriteError(t *testing.T) {
   977  	failAfter, writeCount := 0, 0
   978  	errFail := errors.New("fake write failure")
   979  
   980  	// w is the buffered io.Writer to write the request to. It
   981  	// fails exactly once on its Nth Write call, as controlled by
   982  	// failAfter. It also tracks the number of calls in
   983  	// writeCount.
   984  	w := struct {
   985  		io.ByteWriter // to avoid being wrapped by a bufio.Writer
   986  		io.Writer
   987  	}{
   988  		nil,
   989  		writerFunc(func(p []byte) (n int, err error) {
   990  			writeCount++
   991  			if failAfter == 0 {
   992  				err = errFail
   993  			}
   994  			failAfter--
   995  			return len(p), err
   996  		}),
   997  	}
   998  
   999  	req, _ := NewRequest("GET", "http://example.com/", nil)
  1000  	const writeCalls = 4 // number of Write calls in current implementation
  1001  	sawGood := false
  1002  	for n := 0; n <= writeCalls+2; n++ {
  1003  		failAfter = n
  1004  		writeCount = 0
  1005  		err := req.Write(w)
  1006  		var wantErr error
  1007  		if n < writeCalls {
  1008  			wantErr = errFail
  1009  		}
  1010  		if err != wantErr {
  1011  			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
  1012  			continue
  1013  		}
  1014  		if err == nil {
  1015  			sawGood = true
  1016  			if writeCount != writeCalls {
  1017  				t.Fatalf("writeCalls constant is outdated in test")
  1018  			}
  1019  		}
  1020  		if writeCount > writeCalls || writeCount > n+1 {
  1021  			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
  1022  		}
  1023  	}
  1024  	if !sawGood {
  1025  		t.Fatalf("writeCalls constant is outdated in test")
  1026  	}
  1027  }
  1028  
  1029  // dumpRequestOut is a modified copy of net/http/httputil.DumpRequestOut.
  1030  // Unlike the original, this version doesn't mutate the req.Body and
  1031  // try to restore it. It always dumps the whole body.
  1032  // And it doesn't support https.
  1033  func dumpRequestOut(req *Request, onReadHeaders func()) ([]byte, error) {
  1034  
  1035  	// Use the actual Transport code to record what we would send
  1036  	// on the wire, but not using TCP.  Use a Transport with a
  1037  	// custom dialer that returns a fake net.Conn that waits
  1038  	// for the full input (and recording it), and then responds
  1039  	// with a dummy response.
  1040  	var buf bytes.Buffer // records the output
  1041  	pr, pw := io.Pipe()
  1042  	defer pr.Close()
  1043  	defer pw.Close()
  1044  	dr := &delegateReader{c: make(chan io.Reader)}
  1045  
  1046  	t := &Transport{
  1047  		Dial: func(net, addr string) (net.Conn, error) {
  1048  			return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil
  1049  		},
  1050  	}
  1051  	defer t.CloseIdleConnections()
  1052  
  1053  	// Wait for the request before replying with a dummy response:
  1054  	go func() {
  1055  		req, err := ReadRequest(bufio.NewReader(pr))
  1056  		if err == nil {
  1057  			if onReadHeaders != nil {
  1058  				onReadHeaders()
  1059  			}
  1060  			// Ensure all the body is read; otherwise
  1061  			// we'll get a partial dump.
  1062  			io.Copy(io.Discard, req.Body)
  1063  			req.Body.Close()
  1064  		}
  1065  		dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n")
  1066  	}()
  1067  
  1068  	_, err := t.RoundTrip(req)
  1069  	if err != nil {
  1070  		return nil, err
  1071  	}
  1072  	return buf.Bytes(), nil
  1073  }
  1074  
  1075  // delegateReader is a reader that delegates to another reader,
  1076  // once it arrives on a channel.
  1077  type delegateReader struct {
  1078  	c chan io.Reader
  1079  	r io.Reader // nil until received from c
  1080  }
  1081  
  1082  func (r *delegateReader) Read(p []byte) (int, error) {
  1083  	if r.r == nil {
  1084  		r.r = <-r.c
  1085  	}
  1086  	return r.r.Read(p)
  1087  }
  1088  
  1089  // dumpConn is a net.Conn that writes to Writer and reads from Reader.
  1090  type dumpConn struct {
  1091  	io.Writer
  1092  	io.Reader
  1093  }
  1094  
  1095  func (c *dumpConn) Close() error                       { return nil }
  1096  func (c *dumpConn) LocalAddr() net.Addr                { return nil }
  1097  func (c *dumpConn) RemoteAddr() net.Addr               { return nil }
  1098  func (c *dumpConn) SetDeadline(t time.Time) error      { return nil }
  1099  func (c *dumpConn) SetReadDeadline(t time.Time) error  { return nil }
  1100  func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }
  1101  

View as plain text