github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/reverse_proxy_test.go (about)

     1  package gateway
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"net/url"
    11  	"strings"
    12  	"testing"
    13  	"text/template"
    14  	"time"
    15  
    16  	"github.com/TykTechnologies/tyk/apidef"
    17  	"github.com/TykTechnologies/tyk/config"
    18  	"github.com/TykTechnologies/tyk/ctx"
    19  	"github.com/TykTechnologies/tyk/dnscache"
    20  	"github.com/TykTechnologies/tyk/request"
    21  	"github.com/TykTechnologies/tyk/test"
    22  )
    23  
    24  func TestCopyHeader_NoDuplicateCORSHeaders(t *testing.T) {
    25  
    26  	makeHeaders := func(withCORS bool) http.Header {
    27  
    28  		var h = http.Header{}
    29  
    30  		h.Set("Vary", "Origin")
    31  		h.Set("Location", "https://tyk.io")
    32  
    33  		if withCORS {
    34  			for _, v := range corsHeaders {
    35  				h.Set(v, "tyk.io")
    36  			}
    37  		}
    38  
    39  		return h
    40  	}
    41  
    42  	tests := []struct {
    43  		src, dst http.Header
    44  	}{
    45  		{makeHeaders(true), makeHeaders(false)},
    46  		{makeHeaders(true), makeHeaders(true)},
    47  		{makeHeaders(false), makeHeaders(true)},
    48  	}
    49  
    50  	for _, v := range tests {
    51  		copyHeader(v.dst, v.src)
    52  
    53  		for _, vv := range corsHeaders {
    54  			val := v.dst[vv]
    55  			if n := len(val); n != 1 {
    56  				t.Fatalf("%s found %d times", vv, n)
    57  			}
    58  
    59  		}
    60  
    61  	}
    62  }
    63  
    64  func TestReverseProxyRetainHost(t *testing.T) {
    65  	target, _ := url.Parse("http://target-host.com/targetpath")
    66  	cases := []struct {
    67  		name          string
    68  		inURL, inPath string
    69  		retainHost    bool
    70  		wantURL       string
    71  	}{
    72  		{
    73  			"no-retain-same-path",
    74  			"http://orig-host.com/origpath", "/origpath",
    75  			false, "http://target-host.com/targetpath/origpath",
    76  		},
    77  		{
    78  			"no-retain-minus-slash",
    79  			"http://orig-host.com/origpath", "origpath",
    80  			false, "http://target-host.com/targetpath/origpath",
    81  		},
    82  		{
    83  			"retain-same-path",
    84  			"http://orig-host.com/origpath", "/origpath",
    85  			true, "http://orig-host.com/origpath",
    86  		},
    87  		{
    88  			"retain-minus-slash",
    89  			"http://orig-host.com/origpath", "origpath",
    90  			true, "http://orig-host.com/origpath",
    91  		},
    92  	}
    93  	for _, tc := range cases {
    94  		t.Run(tc.name, func(t *testing.T) {
    95  			spec := &APISpec{APIDefinition: &apidef.APIDefinition{}, URLRewriteEnabled: true}
    96  			spec.URLRewriteEnabled = true
    97  
    98  			req := TestReq(t, http.MethodGet, tc.inURL, nil)
    99  			req.URL.Path = tc.inPath
   100  			if tc.retainHost {
   101  				setCtxValue(req, ctx.RetainHost, true)
   102  			}
   103  
   104  			proxy := TykNewSingleHostReverseProxy(target, spec, nil)
   105  			proxy.Director(req)
   106  
   107  			if got := req.URL.String(); got != tc.wantURL {
   108  				t.Fatalf("wanted url %q, got %q", tc.wantURL, got)
   109  			}
   110  		})
   111  	}
   112  }
   113  
   114  type configTestReverseProxyDnsCache struct {
   115  	*testing.T
   116  
   117  	etcHostsMap map[string][]string
   118  	dnsConfig   config.DnsCacheConfig
   119  }
   120  
   121  func setupTestReverseProxyDnsCache(cfg *configTestReverseProxyDnsCache) func() {
   122  	pullDomains := mockHandle.PushDomains(cfg.etcHostsMap, nil)
   123  	dnsCacheManager.InitDNSCaching(
   124  		time.Duration(cfg.dnsConfig.TTL)*time.Second, time.Duration(cfg.dnsConfig.CheckInterval)*time.Second)
   125  
   126  	globalConf := config.Global()
   127  	enableWebSockets := globalConf.HttpServerOptions.EnableWebSockets
   128  
   129  	globalConf.HttpServerOptions.EnableWebSockets = true
   130  	config.SetGlobal(globalConf)
   131  
   132  	return func() {
   133  		pullDomains()
   134  		dnsCacheManager.DisposeCache()
   135  		globalConf.HttpServerOptions.EnableWebSockets = enableWebSockets
   136  		config.SetGlobal(globalConf)
   137  	}
   138  }
   139  
   140  func TestReverseProxyDnsCache(t *testing.T) {
   141  	const (
   142  		host   = "orig-host.com."
   143  		host2  = "orig-host2.com."
   144  		host3  = "orig-host3.com."
   145  		wsHost = "ws.orig-host.com."
   146  
   147  		hostApiUrl       = "http://orig-host.com/origpath"
   148  		host2HttpApiUrl  = "http://orig-host2.com/origpath"
   149  		host2HttpsApiUrl = "https://orig-host2.com/origpath"
   150  		host3ApiUrl      = "https://orig-host3.com/origpath"
   151  		wsHostWsApiUrl   = "ws://ws.orig-host.com/connect"
   152  		wsHostWssApiUrl  = "wss://ws.orig-host.com/connect"
   153  
   154  		cacheTTL            = 5
   155  		cacheUpdateInterval = 10
   156  	)
   157  
   158  	var (
   159  		etcHostsMap = map[string][]string{
   160  			host:   {"127.0.0.10", "127.0.0.20"},
   161  			host2:  {"10.0.20.0", "10.0.20.1", "10.0.20.2"},
   162  			host3:  {"10.0.20.15", "10.0.20.16"},
   163  			wsHost: {"127.0.0.10", "127.0.0.10"},
   164  		}
   165  	)
   166  
   167  	tearDown := setupTestReverseProxyDnsCache(&configTestReverseProxyDnsCache{t, etcHostsMap,
   168  		config.DnsCacheConfig{
   169  			Enabled: true, TTL: cacheTTL, CheckInterval: cacheUpdateInterval,
   170  			MultipleIPsHandleStrategy: config.NoCacheStrategy}})
   171  
   172  	currentStorage := dnsCacheManager.CacheStorage()
   173  	fakeDeleteStorage := &dnscache.MockStorage{
   174  		MockFetchItem: currentStorage.FetchItem,
   175  		MockGet:       currentStorage.Get,
   176  		MockSet:       currentStorage.Set,
   177  		MockDelete: func(key string) {
   178  			//prevent deletion
   179  		},
   180  		MockClear: currentStorage.Clear}
   181  	dnsCacheManager.SetCacheStorage(fakeDeleteStorage)
   182  
   183  	defer tearDown()
   184  
   185  	cases := []struct {
   186  		name string
   187  
   188  		URL     string
   189  		Method  string
   190  		Body    []byte
   191  		Headers http.Header
   192  
   193  		isWebsocket bool
   194  
   195  		expectedIPs    []string
   196  		shouldBeCached bool
   197  		isCacheEnabled bool
   198  	}{
   199  		{
   200  			"Should cache first request to Host1",
   201  			hostApiUrl,
   202  			http.MethodGet, nil, nil,
   203  			false,
   204  			etcHostsMap[host],
   205  			true, true,
   206  		},
   207  		{
   208  			"Should cache first request to Host2",
   209  			host2HttpsApiUrl,
   210  			http.MethodPost, []byte("{ \"param\": \"value\" }"), nil,
   211  			false,
   212  			etcHostsMap[host2],
   213  			true, true,
   214  		},
   215  		{
   216  			"Should populate from cache second request to Host1",
   217  			hostApiUrl,
   218  			http.MethodGet, nil, nil,
   219  			false,
   220  			etcHostsMap[host],
   221  			false, true,
   222  		},
   223  		{
   224  			"Should populate from cache second request to Host2 with different protocol",
   225  			host2HttpApiUrl,
   226  			http.MethodPost, []byte("{ \"param\": \"value2\" }"), nil,
   227  			false,
   228  			etcHostsMap[host2],
   229  			false, true,
   230  		},
   231  		{
   232  			"Shouldn't cache request with different http verb to same host",
   233  			hostApiUrl,
   234  			http.MethodPatch, []byte("{ \"param2\": \"value3\" }"), nil,
   235  			false,
   236  			etcHostsMap[host],
   237  			false, true,
   238  		},
   239  		{
   240  			"Shouldn't cache dns record when cache is disabled",
   241  			host3ApiUrl,
   242  			http.MethodGet, nil, nil,
   243  			false, etcHostsMap[host3],
   244  			false, false,
   245  		},
   246  		{
   247  			"Should cache ws protocol host dns records",
   248  			wsHostWsApiUrl,
   249  			http.MethodGet, nil,
   250  			map[string][]string{
   251  				"Upgrade":    {"websocket"},
   252  				"Connection": {"Upgrade"},
   253  			},
   254  			true,
   255  			etcHostsMap[wsHost],
   256  			true, true,
   257  		},
   258  		// {
   259  		// 	"Should cache wss protocol host dns records",
   260  		// 	wsHostWssApiUrl,
   261  		// 	http.MethodGet, nil,
   262  		// 	map[string][]string{
   263  		// 		"Upgrade":    {"websocket"},
   264  		// 		"Connection": {"Upgrade"},
   265  		// 	},
   266  		// 	true,
   267  		// 	etcHostsMap[wsHost],
   268  		// 	true, true,
   269  		// },
   270  	}
   271  	for _, tc := range cases {
   272  		t.Run(tc.name, func(t *testing.T) {
   273  			storage := dnsCacheManager.CacheStorage()
   274  			if !tc.isCacheEnabled {
   275  				dnsCacheManager.SetCacheStorage(nil)
   276  			}
   277  
   278  			spec := &APISpec{APIDefinition: &apidef.APIDefinition{},
   279  				EnforcedTimeoutEnabled: true,
   280  				GlobalConfig:           config.Config{ProxyCloseConnections: true, ProxyDefaultTimeout: 0.1}}
   281  
   282  			req := TestReq(t, tc.Method, tc.URL, tc.Body)
   283  			for name, value := range tc.Headers {
   284  				req.Header.Add(name, strings.Join(value, ";"))
   285  			}
   286  
   287  			Url, _ := url.Parse(tc.URL)
   288  			proxy := TykNewSingleHostReverseProxy(Url, spec, nil)
   289  			recorder := httptest.NewRecorder()
   290  			proxy.WrappedServeHTTP(recorder, req, false)
   291  
   292  			host := Url.Hostname()
   293  			if tc.isCacheEnabled {
   294  				item, ok := storage.Get(host)
   295  				if !ok || !test.IsDnsRecordsAddrsEqualsTo(item.Addrs, tc.expectedIPs) {
   296  					t.Fatalf("got %q, but wanted %q. ok=%t", item, tc.expectedIPs, ok)
   297  				}
   298  			} else {
   299  				item, ok := storage.Get(host)
   300  				if ok {
   301  					t.Fatalf("got %t, but wanted %t. item=%#v", ok, false, item)
   302  				}
   303  			}
   304  
   305  			if !tc.isCacheEnabled {
   306  				dnsCacheManager.SetCacheStorage(storage)
   307  			}
   308  		})
   309  	}
   310  }
   311  
   312  func testNewWrappedServeHTTP() *ReverseProxy {
   313  	target, _ := url.Parse(TestHttpGet)
   314  	def := apidef.APIDefinition{}
   315  	def.VersionData.DefaultVersion = "Default"
   316  	def.VersionData.Versions = map[string]apidef.VersionInfo{
   317  		"Default": {
   318  			Name:             "v2",
   319  			UseExtendedPaths: true,
   320  			ExtendedPaths: apidef.ExtendedPathsSet{
   321  				TransformHeader: []apidef.HeaderInjectionMeta{
   322  					{
   323  						DeleteHeaders: []string{"header"},
   324  						AddHeaders:    map[string]string{"newheader": "newvalue"},
   325  						Path:          "/abc",
   326  						Method:        "GET",
   327  						ActOnResponse: true,
   328  					},
   329  				},
   330  				URLRewrite: []apidef.URLRewriteMeta{
   331  					{
   332  						Path:         "/get",
   333  						Method:       "GET",
   334  						MatchPattern: "/get",
   335  						RewriteTo:    "/post",
   336  					},
   337  				},
   338  			},
   339  		},
   340  	}
   341  	spec := &APISpec{
   342  		APIDefinition:          &def,
   343  		EnforcedTimeoutEnabled: true,
   344  		CircuitBreakerEnabled:  true,
   345  	}
   346  	return TykNewSingleHostReverseProxy(target, spec, nil)
   347  }
   348  
   349  func TestWrappedServeHTTP(t *testing.T) {
   350  	proxy := testNewWrappedServeHTTP()
   351  	recorder := httptest.NewRecorder()
   352  	req, _ := http.NewRequest(http.MethodGet, "/", nil)
   353  	proxy.WrappedServeHTTP(recorder, req, false)
   354  }
   355  
   356  func TestCircuitBreaker5xxs(t *testing.T) {
   357  	ts := StartTest()
   358  	defer ts.Close()
   359  
   360  	t.Run("Extended Paths", func(t *testing.T) {
   361  		BuildAndLoadAPI(func(spec *APISpec) {
   362  			UpdateAPIVersion(spec, "v1", func(v *apidef.VersionInfo) {
   363  				json.Unmarshal([]byte(`[
   364  					{
   365  						"path": "error",
   366  						"method": "GET",
   367  						"threshold_percent": 0.1,
   368  						"samples": 3,
   369  						"return_to_service_after": 6000
   370  					}
   371    			 	]`), &v.ExtendedPaths.CircuitBreaker)
   372  			})
   373  			spec.Proxy.ListenPath = "/"
   374  			spec.CircuitBreakerEnabled = true
   375  		})
   376  
   377  		ts.Run(t, []test.TestCase{
   378  			{Path: "/errors/500", Code: http.StatusInternalServerError},
   379  			{Path: "/errors/501", Code: http.StatusNotImplemented},
   380  			{Path: "/errors/502", Code: http.StatusBadGateway},
   381  			{Path: "/errors/500", Code: http.StatusServiceUnavailable},
   382  			{Path: "/errors/501", Code: http.StatusServiceUnavailable},
   383  			{Path: "/errors/502", Code: http.StatusServiceUnavailable},
   384  		}...)
   385  	})
   386  }
   387  
   388  func TestSingleJoiningSlash(t *testing.T) {
   389  	testsFalse := []struct {
   390  		a, b, want string
   391  	}{
   392  		{"foo", "", "foo"},
   393  		{"foo", "bar", "foo/bar"},
   394  		{"foo/", "bar", "foo/bar"},
   395  		{"foo", "/bar", "foo/bar"},
   396  		{"foo/", "/bar", "foo/bar"},
   397  		{"foo//", "//bar", "foo/bar"},
   398  	}
   399  	for _, tc := range testsFalse {
   400  		t.Run(fmt.Sprintf("%s+%s", tc.a, tc.b), func(t *testing.T) {
   401  			got := singleJoiningSlash(tc.a, tc.b, false)
   402  			if got != tc.want {
   403  				t.Fatalf("want %s, got %s", tc.want, got)
   404  			}
   405  		})
   406  	}
   407  	testsTrue := []struct {
   408  		a, b, want string
   409  	}{
   410  		{"foo/", "", "foo/"},
   411  		{"foo", "", "foo"},
   412  	}
   413  	for _, tc := range testsTrue {
   414  		t.Run(fmt.Sprintf("%s+%s", tc.a, tc.b), func(t *testing.T) {
   415  			got := singleJoiningSlash(tc.a, tc.b, true)
   416  			if got != tc.want {
   417  				t.Fatalf("want %s, got %s", tc.want, got)
   418  			}
   419  		})
   420  	}
   421  }
   422  
   423  func TestRequestIP(t *testing.T) {
   424  	tests := []struct {
   425  		remote, real, forwarded, want string
   426  	}{
   427  		// missing ip or port
   428  		{want: ""},
   429  		{remote: ":80", want: ""},
   430  		{remote: "1.2.3.4", want: ""},
   431  		{remote: "[::1]", want: ""},
   432  		// no headers
   433  		{remote: "1.2.3.4:80", want: "1.2.3.4"},
   434  		{remote: "[::1]:80", want: "::1"},
   435  		// real-ip
   436  		{
   437  			remote: "1.2.3.4:80",
   438  			real:   "5.6.7.8",
   439  			want:   "5.6.7.8",
   440  		},
   441  		{
   442  			remote: "[::1]:80",
   443  			real:   "::2",
   444  			want:   "::2",
   445  		},
   446  		// forwarded-for
   447  		{
   448  			remote:    "1.2.3.4:80",
   449  			forwarded: "5.6.7.8, px1, px2",
   450  			want:      "5.6.7.8",
   451  		},
   452  		{
   453  			remote:    "[::1]:80",
   454  			forwarded: "::2",
   455  			want:      "::2",
   456  		},
   457  		// both real-ip and forwarded-for
   458  		{
   459  			remote:    "1.2.3.4:80",
   460  			real:      "5.6.7.8",
   461  			forwarded: "4.3.2.1, px1, px2",
   462  			want:      "5.6.7.8",
   463  		},
   464  	}
   465  	for _, tc := range tests {
   466  		r := &http.Request{RemoteAddr: tc.remote, Header: http.Header{}}
   467  		r.Header.Set("x-real-ip", tc.real)
   468  		r.Header.Set("x-forwarded-for", tc.forwarded)
   469  		got := request.RealIP(r)
   470  		if got != tc.want {
   471  			t.Errorf("requestIP({%q, %q, %q}) got %q, want %q",
   472  				tc.remote, tc.real, tc.forwarded, got, tc.want)
   473  		}
   474  	}
   475  }
   476  
   477  func TestCheckHeaderInRemoveList(t *testing.T) {
   478  	type testSpec struct {
   479  		UseExtendedPaths      bool
   480  		GlobalHeadersRemove   []string
   481  		ExtendedDeleteHeaders []string
   482  	}
   483  	tpl, err := template.New("test_tpl").Parse(`{
   484  		"api_id": "1",
   485  		"version_data": {
   486  			"not_versioned": true,
   487  			"versions": {
   488  				"Default": {
   489  					"name": "Default",
   490  					"use_extended_paths": {{ .UseExtendedPaths }},
   491  					"global_headers_remove": [{{ range $index, $hdr := .GlobalHeadersRemove }}{{if $index}}, {{end}}{{print "\"" . "\"" }}{{end}}],
   492  					"extended_paths": {
   493  						"transform_headers": [{
   494  							"delete_headers": [{{range $index, $hdr := .ExtendedDeleteHeaders}}{{if $index}}, {{end}}{{print "\"" . "\""}}{{end}}],
   495  							"path": "test",
   496  							"method": "GET"
   497  						}]
   498  					}
   499  				}
   500  			}
   501  		}
   502  	}`)
   503  	if err != nil {
   504  		t.Fatal(err)
   505  	}
   506  
   507  	tests := []struct {
   508  		header   string
   509  		spec     testSpec
   510  		expected bool
   511  	}{
   512  		{
   513  			header: "X-Forwarded-For",
   514  		},
   515  		{
   516  			header: "X-Forwarded-For",
   517  			spec:   testSpec{GlobalHeadersRemove: []string{"X-Random-Header"}},
   518  		},
   519  		{
   520  			header: "X-Forwarded-For",
   521  			spec: testSpec{
   522  				UseExtendedPaths:      true,
   523  				ExtendedDeleteHeaders: []string{"X-Random-Header"},
   524  			},
   525  		},
   526  		{
   527  			header:   "X-Forwarded-For",
   528  			spec:     testSpec{GlobalHeadersRemove: []string{"X-Forwarded-For"}},
   529  			expected: true,
   530  		},
   531  		{
   532  			header: "X-Forwarded-For",
   533  			spec: testSpec{
   534  				UseExtendedPaths:      true,
   535  				GlobalHeadersRemove:   []string{"X-Random-Header"},
   536  				ExtendedDeleteHeaders: []string{"X-Forwarded-For"},
   537  			},
   538  			expected: true,
   539  		},
   540  		{
   541  			header: "X-Forwarded-For",
   542  			spec: testSpec{
   543  				UseExtendedPaths:      true,
   544  				GlobalHeadersRemove:   []string{"X-Forwarded-For"},
   545  				ExtendedDeleteHeaders: []string{"X-Forwarded-For"},
   546  			},
   547  			expected: true,
   548  		},
   549  	}
   550  
   551  	for _, tc := range tests {
   552  		t.Run(fmt.Sprintf("%s:%t", tc.header, tc.expected), func(t *testing.T) {
   553  			rp := &ReverseProxy{}
   554  			r, err := http.NewRequest(http.MethodGet, "http://test/test", nil)
   555  			if err != nil {
   556  				t.Fatal(err)
   557  			}
   558  
   559  			var specOutput bytes.Buffer
   560  			if err := tpl.Execute(&specOutput, tc.spec); err != nil {
   561  				t.Fatal(err)
   562  			}
   563  
   564  			spec := LoadSampleAPI(specOutput.String())
   565  			actual := rp.CheckHeaderInRemoveList(tc.header, spec, r)
   566  			if actual != tc.expected {
   567  				t.Fatalf("want %t, got %t", tc.expected, actual)
   568  			}
   569  		})
   570  	}
   571  }
   572  
   573  func testRequestIPHops(t testing.TB) {
   574  	req := &http.Request{
   575  		Header:     http.Header{},
   576  		RemoteAddr: "test.com:80",
   577  	}
   578  	req.Header.Set("X-Forwarded-For", "abc")
   579  	match := "abc, test.com"
   580  	clientIP := requestIPHops(req)
   581  	if clientIP != match {
   582  		t.Fatalf("Got %s, expected %s", clientIP, match)
   583  	}
   584  }
   585  
   586  func TestRequestIPHops(t *testing.T) {
   587  	testRequestIPHops(t)
   588  }
   589  
   590  func TestNopCloseRequestBody(t *testing.T) {
   591  	// try to pass nil request
   592  	var req *http.Request
   593  	nopCloseRequestBody(req)
   594  	if req != nil {
   595  		t.Error("nil Request should remain nil")
   596  	}
   597  
   598  	// try to pass nil body
   599  	req = &http.Request{}
   600  	nopCloseRequestBody(req)
   601  	if req.Body != nil {
   602  		t.Error("Request nil body should remain nil")
   603  	}
   604  
   605  	// try to pass not nil body and check that it was replaced with nopCloser
   606  	req = httptest.NewRequest(http.MethodGet, "/test", strings.NewReader("abcxyz"))
   607  	nopCloseRequestBody(req)
   608  	if body, ok := req.Body.(nopCloser); !ok {
   609  		t.Error("Request's body was not replaced with nopCloser")
   610  	} else {
   611  		// try to read body 1st time
   612  		if data, err := ioutil.ReadAll(body); err != nil {
   613  			t.Error("1st read, error while reading body:", err)
   614  		} else if !bytes.Equal(data, []byte("abcxyz")) { // compare with expected data
   615  			t.Error("1st read, body's data is not as expectd")
   616  		}
   617  
   618  		// try to read body again without closing
   619  		if data, err := ioutil.ReadAll(body); err != nil {
   620  			t.Error("2nd read, error while reading body:", err)
   621  		} else if !bytes.Equal(data, []byte("abcxyz")) { // compare with expected data
   622  			t.Error("2nd read, body's data is not as expectd")
   623  		}
   624  
   625  		// close body and try to read "closed" one
   626  		body.Close()
   627  		if data, err := ioutil.ReadAll(body); err != nil {
   628  			t.Error("3rd read, error while reading body:", err)
   629  		} else if !bytes.Equal(data, []byte("abcxyz")) { // compare with expected data
   630  			t.Error("3rd read, body's data is not as expectd")
   631  		}
   632  	}
   633  }
   634  
   635  func TestNopCloseResponseBody(t *testing.T) {
   636  	var resp *http.Response
   637  	nopCloseResponseBody(resp)
   638  	if resp != nil {
   639  		t.Error("nil Response should remain nil")
   640  	}
   641  
   642  	// try to pass nil body
   643  	resp = &http.Response{}
   644  	nopCloseResponseBody(resp)
   645  	if resp.Body != nil {
   646  		t.Error("Response nil body should remain nil")
   647  	}
   648  
   649  	// try to pass not nil body and check that it was replaced with nopCloser
   650  	resp = &http.Response{}
   651  	resp.Body = ioutil.NopCloser(strings.NewReader("abcxyz"))
   652  	nopCloseResponseBody(resp)
   653  	if body, ok := resp.Body.(nopCloser); !ok {
   654  		t.Error("Response's body was not replaced with nopCloser")
   655  	} else {
   656  		// try to read body 1st time
   657  		if data, err := ioutil.ReadAll(body); err != nil {
   658  			t.Error("1st read, error while reading body:", err)
   659  		} else if !bytes.Equal(data, []byte("abcxyz")) { // compare with expected data
   660  			t.Error("1st read, body's data is not as expectd")
   661  		}
   662  
   663  		// try to read body again without closing
   664  		if data, err := ioutil.ReadAll(body); err != nil {
   665  			t.Error("2nd read, error while reading body:", err)
   666  		} else if !bytes.Equal(data, []byte("abcxyz")) { // compare with expected data
   667  			t.Error("2nd read, body's data is not as expectd")
   668  		}
   669  
   670  		// close body and try to read "closed" one
   671  		body.Close()
   672  		if data, err := ioutil.ReadAll(body); err != nil {
   673  			t.Error("3rd read, error while reading body:", err)
   674  		} else if !bytes.Equal(data, []byte("abcxyz")) { // compare with expected data
   675  			t.Error("3rd read, body's data is not as expectd")
   676  		}
   677  	}
   678  }
   679  
   680  func BenchmarkRequestIPHops(b *testing.B) {
   681  	b.ReportAllocs()
   682  	for i := 0; i < b.N; i++ {
   683  		testRequestIPHops(b)
   684  	}
   685  }
   686  
   687  func BenchmarkWrappedServeHTTP(b *testing.B) {
   688  	b.ReportAllocs()
   689  	proxy := testNewWrappedServeHTTP()
   690  	recorder := httptest.NewRecorder()
   691  	req, _ := http.NewRequest(http.MethodGet, "/", nil)
   692  	for i := 0; i < b.N; i++ {
   693  		proxy.WrappedServeHTTP(recorder, req, false)
   694  	}
   695  }
   696  
   697  func BenchmarkCopyRequestResponse(b *testing.B) {
   698  	b.ReportAllocs()
   699  
   700  	str := strings.Repeat("very long body line that is repeated", 128)
   701  	req := &http.Request{}
   702  	res := &http.Response{}
   703  	for i := 0; i < b.N; i++ {
   704  		req.Body = ioutil.NopCloser(strings.NewReader(str))
   705  		res.Body = ioutil.NopCloser(strings.NewReader(str))
   706  		for j := 0; j < 10; j++ {
   707  			req = copyRequest(req)
   708  			res = copyResponse(res)
   709  		}
   710  	}
   711  }