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 }