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