github.com/avenga/couper@v1.12.2/server/http_integration_test.go (about) 1 package server_test 2 3 import ( 4 "bufio" 5 "bytes" 6 "compress/gzip" 7 "context" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net" 12 "net/http" 13 "net/http/httptest" 14 "net/textproto" 15 "net/url" 16 "os" 17 "path" 18 "path/filepath" 19 "reflect" 20 "sort" 21 "strconv" 22 "strings" 23 "sync" 24 "testing" 25 "text/template" 26 "time" 27 28 "github.com/golang-jwt/jwt/v4" 29 "github.com/google/go-cmp/cmp" 30 "github.com/sirupsen/logrus" 31 logrustest "github.com/sirupsen/logrus/hooks/test" 32 33 "github.com/avenga/couper/command" 34 "github.com/avenga/couper/config" 35 "github.com/avenga/couper/config/configload" 36 "github.com/avenga/couper/config/env" 37 "github.com/avenga/couper/errors" 38 "github.com/avenga/couper/internal/test" 39 "github.com/avenga/couper/logging" 40 "github.com/avenga/couper/oauth2" 41 ) 42 43 var ( 44 testBackend *test.Backend 45 testWorkingDir string 46 testProxyAddr = "http://127.0.0.1:9999" 47 testServerMu = sync.Mutex{} 48 ) 49 50 func TestMain(m *testing.M) { 51 setup() 52 code := m.Run() 53 teardown() 54 os.Exit(code) 55 } 56 57 func setup() { 58 println("INTEGRATION: create test backend...") 59 testBackend = test.NewBackend() 60 err := os.Setenv("COUPER_TEST_BACKEND_ADDR", testBackend.Addr()) 61 if err != nil { 62 panic(err) 63 } 64 65 err = os.Setenv("HTTP_PROXY", testProxyAddr) 66 if err != nil { 67 panic(err) 68 } 69 70 wd, err := os.Getwd() 71 if err != nil { 72 panic(err) 73 } 74 testWorkingDir = wd 75 } 76 77 func teardown() { 78 println("INTEGRATION: close test backend...") 79 for _, key := range []string{"COUPER_TEST_BACKEND_ADDR", "HTTP_PROXY"} { 80 if err := os.Unsetenv(key); err != nil { 81 panic(err) 82 } 83 } 84 testBackend.Close() 85 } 86 87 func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) { 88 couperConfig, err := configload.LoadFile(filepath.Join(testWorkingDir, file), "test") 89 helper.Must(err) 90 91 return newCouperWithConfig(couperConfig, helper) 92 } 93 94 func newCouperMultiFiles(file, dir string, helper *test.Helper) (func(), *logrustest.Hook) { 95 couperConfig, err := configload.LoadFiles([]string{file, dir}, "test", nil) 96 helper.Must(err) 97 98 return newCouperWithConfig(couperConfig, helper) 99 } 100 101 // newCouperWithTemplate applies given variables first and loads Couper with the resulting configuration file. 102 // Example template: 103 // 104 // My {{.message}} 105 // 106 // Example value: 107 // 108 // map[string]interface{}{ 109 // "message": "value", 110 // } 111 func newCouperWithTemplate(file string, helper *test.Helper, vars map[string]interface{}) (func(), *logrustest.Hook, error) { 112 if vars == nil { 113 s, h := newCouper(file, helper) 114 return s, h, nil 115 } 116 117 tpl, err := template.New(filepath.Base(file)).ParseFiles(file) 118 helper.Must(err) 119 120 result := &bytes.Buffer{} 121 helper.Must(tpl.Execute(result, vars)) 122 123 return newCouperWithBytes(result.Bytes(), helper) 124 } 125 126 func newCouperWithBytes(file []byte, helper *test.Helper) (func(), *logrustest.Hook, error) { 127 couperConfig, err := configload.LoadBytes(file, "couper-bytes.hcl") 128 if err != nil { 129 return nil, nil, err 130 } 131 s, h := newCouperWithConfig(couperConfig, helper) 132 return s, h, nil 133 } 134 135 func newCouperWithConfig(couperConfig *config.Couper, helper *test.Helper) (func(), *logrustest.Hook) { 136 testServerMu.Lock() 137 defer testServerMu.Unlock() 138 139 log, hook := test.NewLogger() 140 log.Level, _ = logrus.ParseLevel(couperConfig.Settings.LogLevel) 141 142 ctx, cancelFn := context.WithCancel(context.Background()) 143 shutdownFn := func() { 144 if helper.TestFailed() { // log on error 145 time.Sleep(time.Second) 146 for _, entry := range hook.AllEntries() { 147 s, _ := entry.String() 148 helper.Logf(s) 149 } 150 } 151 cleanup(cancelFn, helper) 152 } 153 154 // ensure the previous test aren't listening 155 port := couperConfig.Settings.DefaultPort 156 test.WaitForClosedPort(port) 157 waitForCh := make(chan struct{}, 1) 158 command.RunCmdTestCallback = func() { 159 waitForCh <- struct{}{} 160 } 161 defer func() { command.RunCmdTestCallback = nil }() 162 163 go func() { 164 if err := command.NewRun(ctx).Execute(nil, couperConfig, log.WithContext(ctx)); err != nil { 165 command.RunCmdTestCallback() 166 shutdownFn() 167 if lerr, ok := err.(*errors.Error); ok { 168 panic(lerr.LogError()) 169 } else { 170 panic(err) 171 } 172 } 173 }() 174 <-waitForCh 175 176 for _, entry := range hook.AllEntries() { 177 if entry.Level < logrus.WarnLevel { 178 // ignore health-check startup errors 179 if req, ok := entry.Data["request"]; ok { 180 if reqFields, ok := req.(logging.Fields); ok { 181 n := reqFields["name"] 182 if hc, ok := n.(string); ok && hc == "health-check" { 183 continue 184 } 185 } 186 } 187 defer os.Exit(1) // ok in loop, next line is the end 188 helper.Must(fmt.Errorf("error: %#v: %s", entry.Data, entry.Message)) 189 } 190 } 191 192 hook.Reset() // no startup logs 193 return shutdownFn, hook 194 } 195 196 func newClient() *http.Client { 197 return test.NewHTTPClient() 198 } 199 200 func cleanup(shutdown func(), helper *test.Helper) { 201 testServerMu.Lock() 202 defer testServerMu.Unlock() 203 204 shutdown() 205 206 err := os.Chdir(testWorkingDir) 207 if err != nil { 208 helper.Must(err) 209 } 210 } 211 212 func TestHTTPServer_ServeHTTP(t *testing.T) { 213 type testRequest struct { 214 method, url string 215 } 216 217 type expectation struct { 218 status int 219 body []byte 220 header http.Header 221 handlerName string 222 } 223 224 type requestCase struct { 225 req testRequest 226 exp expectation 227 } 228 229 type testCase struct { 230 fileName string 231 requests []requestCase 232 } 233 234 client := newClient() 235 236 for i, testcase := range []testCase{ 237 {"spa/01_couper.hcl", []requestCase{ 238 { 239 testRequest{http.MethodGet, "http://anyserver:8080/"}, 240 expectation{http.StatusOK, []byte(`<html><body><title>1.0</title></body></html>`), nil, "spa"}, 241 }, 242 { 243 testRequest{http.MethodGet, "http://anyserver:8080/app"}, 244 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 245 }, 246 }}, 247 {"files/01_couper.hcl", []requestCase{ 248 { 249 testRequest{http.MethodGet, "http://anyserver:8080/"}, 250 expectation{http.StatusOK, []byte(`<html lang="en">index</html>`), nil, "file"}, 251 }, 252 }}, 253 {"files/02_couper.hcl", []requestCase{ 254 { 255 testRequest{http.MethodGet, "http://anyserver:8080/a"}, 256 expectation{http.StatusOK, []byte(`<html lang="en">index A</html>`), nil, "file"}, 257 }, 258 { 259 testRequest{http.MethodGet, "http://couper.io:9898/a"}, 260 expectation{http.StatusOK, []byte(`<html lang="en">index A</html>`), nil, "file"}, 261 }, 262 { 263 testRequest{http.MethodGet, "http://couper.io:9898/"}, 264 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 265 }, 266 { 267 testRequest{http.MethodGet, "http://example.com:9898/b"}, 268 expectation{http.StatusOK, []byte(`<html lang="en">index B</html>`), nil, "file"}, 269 }, 270 { 271 testRequest{http.MethodGet, "http://example.com:9898/"}, 272 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 273 }, 274 }}, 275 {"files_spa_api/01_couper.hcl", []requestCase{ 276 { 277 testRequest{http.MethodGet, "http://anyserver:8080/"}, 278 expectation{http.StatusOK, []byte("<html><body><title>SPA_01</title>{\"default\":\"true\"}</body></html>\n"), nil, "spa"}, 279 }, 280 { 281 testRequest{http.MethodGet, "http://anyserver:8080/foo"}, 282 expectation{http.StatusOK, []byte("<html><body><title>SPA_01</title>{\"default\":\"true\"}</body></html>\n"), nil, "spa"}, 283 }, 284 }}, 285 {"api/01_couper.hcl", []requestCase{ 286 { 287 testRequest{http.MethodGet, "http://anyserver:8080/"}, 288 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 289 }, 290 { 291 testRequest{http.MethodGet, "http://anyserver:8080/v1"}, 292 expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"}, 293 }, 294 { 295 testRequest{http.MethodGet, "http://anyserver:8080/v1/"}, 296 expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"}, 297 }, 298 { 299 testRequest{http.MethodGet, "http://anyserver:8080/v1/not-found"}, 300 expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""}, 301 }, 302 { 303 testRequest{http.MethodGet, "http://anyserver:8080/v1/connect-error/"}, // in this case proxyconnect fails 304 expectation{http.StatusBadGateway, []byte(`{"message": "backend error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, "api"}, 305 }, 306 { 307 testRequest{http.MethodGet, "http://anyserver:8080/v1x"}, 308 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 309 }, 310 }}, 311 {"api/02_couper.hcl", []requestCase{ 312 { 313 testRequest{http.MethodGet, "http://anyserver:8080/"}, 314 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 315 }, 316 { 317 testRequest{http.MethodGet, "http://anyserver:8080/v2/"}, 318 expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"}, 319 }, 320 { 321 testRequest{http.MethodGet, "http://couper.io:9898/v2/"}, 322 expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"}, 323 }, 324 { 325 testRequest{http.MethodGet, "http://example.com:9898/v3/"}, 326 expectation{http.StatusOK, nil, http.Header{"Content-Type": {"application/json"}}, "api"}, 327 }, 328 { 329 testRequest{http.MethodGet, "http://anyserver:8080/v2/not-found"}, 330 expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""}, 331 }, 332 { 333 testRequest{http.MethodGet, "http://couper.io:9898/v2/not-found"}, 334 expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""}, 335 }, 336 { 337 testRequest{http.MethodGet, "http://example.com:9898/v3/not-found"}, 338 expectation{http.StatusNotFound, []byte(`{"message": "route not found error" }` + "\n"), http.Header{"Content-Type": {"application/json"}}, ""}, 339 }, 340 }}, 341 {"vhosts/01_couper.hcl", []requestCase{ 342 { 343 testRequest{http.MethodGet, "http://anyserver:8080/notfound"}, 344 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 345 }, 346 { 347 testRequest{http.MethodGet, "http://anyserver:8080/"}, 348 expectation{http.StatusOK, []byte("<html><body><title>FS_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"}, 349 }, 350 { 351 testRequest{http.MethodGet, "http://anyserver:8080/spa1"}, 352 expectation{http.StatusOK, []byte("<html><body><title>SPA_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "spa"}, 353 }, 354 { 355 testRequest{http.MethodGet, "http://example.com:8080/"}, 356 expectation{http.StatusOK, []byte("<html><body><title>FS_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"}, 357 }, 358 { 359 testRequest{http.MethodGet, "http://example.org:9876/"}, 360 expectation{http.StatusOK, []byte("<html><body><title>FS_01</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"}, 361 }, 362 { 363 testRequest{http.MethodGet, "http://couper.io:8080/"}, 364 expectation{http.StatusOK, []byte("<html><body><title>FS_02</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"}, 365 }, 366 { 367 testRequest{http.MethodGet, "http://couper.io:8080/spa2"}, 368 expectation{http.StatusOK, []byte("<html><body><title>SPA_02</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "spa"}, 369 }, 370 { 371 testRequest{http.MethodGet, "http://example.net:9876/"}, 372 expectation{http.StatusOK, []byte("<html><body><title>FS_02</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"}, 373 }, 374 { 375 testRequest{http.MethodGet, "http://v-server3.com:8080/"}, 376 expectation{http.StatusOK, []byte("<html><body><title>FS_03</title></body></html>\n"), http.Header{"Content-Type": {"text/html; charset=utf-8"}}, "file"}, 377 }, 378 { 379 testRequest{http.MethodGet, "http://v-server3.com:8080/spa2"}, 380 expectation{http.StatusNotFound, []byte("<html>route not found error</html>\n"), http.Header{"Couper-Error": {"route not found error"}}, ""}, 381 }, 382 }}, 383 {"endpoint_eval/16_couper.hcl", []requestCase{ 384 { 385 testRequest{http.MethodGet, "http://anyserver:8080/"}, 386 expectation{http.StatusInternalServerError, []byte("<html>configuration error</html>\n"), http.Header{"Couper-Error": {"configuration error"}}, ""}, 387 }, 388 }}, 389 } { 390 confPath := path.Join("testdata/integration", testcase.fileName) 391 t.Logf("#%.2d: Create Couper: %q", i+1, confPath) 392 393 for _, rc := range testcase.requests { 394 t.Run(testcase.fileName+" "+rc.req.method+"|"+rc.req.url, func(subT *testing.T) { 395 helper := test.New(subT) 396 shutdown, logHook := newCouper(confPath, helper) 397 defer shutdown() 398 399 logHook.Reset() 400 401 req, err := http.NewRequest(rc.req.method, rc.req.url, nil) 402 helper.Must(err) 403 404 res, err := client.Do(req) 405 helper.Must(err) 406 407 resBytes, err := io.ReadAll(res.Body) 408 helper.Must(err) 409 410 _ = res.Body.Close() 411 412 if res.StatusCode != rc.exp.status { 413 subT.Errorf("Expected statusCode %d, got %d", rc.exp.status, res.StatusCode) 414 subT.Logf("Failed: %s|%s", testcase.fileName, rc.req.url) 415 } 416 417 for k, v := range rc.exp.header { 418 if !reflect.DeepEqual(res.Header[k], v) { 419 subT.Errorf("Exptected headers:\nWant:\t%#v\nGot:\t%#v\n", v, res.Header[k]) 420 } 421 } 422 423 if rc.exp.body != nil && !bytes.Equal(resBytes, rc.exp.body) { 424 subT.Errorf("Expected same body content:\nWant:\t%q\nGot:\t%q\n", string(rc.exp.body), string(resBytes)) 425 } 426 427 entry := logHook.LastEntry() 428 429 if entry == nil || entry.Data["type"] != "couper_access" { 430 subT.Error("Expected a log entry, got nothing") 431 return 432 } 433 if handler, ok := entry.Data["handler"]; rc.exp.handlerName != "" && (!ok || handler != rc.exp.handlerName) { 434 subT.Errorf("Expected handler %q within logs, got:\n%#v", rc.exp.handlerName, entry.Data) 435 } 436 }) 437 } 438 } 439 } 440 441 func TestHTTPServer_HostHeader(t *testing.T) { 442 helper := test.New(t) 443 444 client := newClient() 445 446 confPath := path.Join("testdata/integration", "files/02_couper.hcl") 447 shutdown, _ := newCouper(confPath, helper) 448 defer shutdown() 449 450 req, err := http.NewRequest(http.MethodGet, "http://example.com:9898/b", nil) 451 helper.Must(err) 452 453 req.Host = "Example.com." 454 res, err := client.Do(req) 455 helper.Must(err) 456 457 resBytes, err := io.ReadAll(res.Body) 458 helper.Must(err) 459 460 _ = res.Body.Close() 461 462 if string(resBytes) != `<html lang="en">index B</html>` { 463 t.Errorf("%s", resBytes) 464 } 465 } 466 467 func TestHTTPServer_HostHeader2(t *testing.T) { 468 helper := test.New(t) 469 470 client := newClient() 471 472 confPath := path.Join("testdata/integration", "api/03_couper.hcl") 473 shutdown, logHook := newCouper(confPath, helper) 474 defer shutdown() 475 476 req, err := http.NewRequest(http.MethodGet, "http://couper.io:9898/v3/def", nil) 477 helper.Must(err) 478 479 req.Host = "couper.io" 480 res, err := client.Do(req) 481 helper.Must(err) 482 483 resBytes, err := io.ReadAll(res.Body) 484 helper.Must(err) 485 486 _ = res.Body.Close() 487 488 if string(resBytes) != "<html>route not found error</html>\n" { 489 t.Errorf("%s", resBytes) 490 } 491 492 entry := logHook.LastEntry() 493 if entry == nil { 494 t.Error("Expected a log entry, got nothing") 495 } else if entry.Data["server"] != "multi-api-host1" { 496 t.Errorf("Expected 'multi-api-host1', got: %s", entry.Data["server"]) 497 } 498 } 499 500 func TestHTTPServer_EnvVars(t *testing.T) { 501 helper := test.New(t) 502 client := newClient() 503 504 env.SetTestOsEnviron(func() []string { 505 return []string{"BAP1=pass1"} 506 }) 507 defer env.SetTestOsEnviron(os.Environ) 508 509 shutdown, hook := newCouper("testdata/integration/env/01_couper.hcl", test.New(t)) 510 defer shutdown() 511 512 hook.Reset() 513 514 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil) 515 helper.Must(err) 516 517 res, err := client.Do(req) 518 helper.Must(err) 519 520 if res.StatusCode != http.StatusUnauthorized { 521 t.Errorf("expected 401, got %d", res.StatusCode) 522 } 523 } 524 525 func TestHTTPServer_DefaultEnvVars(t *testing.T) { 526 helper := test.New(t) 527 client := newClient() 528 529 env.SetTestOsEnviron(func() []string { 530 return []string{"VALUE_4=value4"} 531 }) 532 defer env.SetTestOsEnviron(os.Environ) 533 534 shutdown, hook := newCouper("testdata/integration/env/02_couper.hcl", test.New(t)) 535 defer shutdown() 536 537 hook.Reset() 538 539 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil) 540 helper.Must(err) 541 542 res, err := client.Do(req) 543 helper.Must(err) 544 545 if res.StatusCode != http.StatusOK { 546 t.Errorf("expected 200, got %d", res.StatusCode) 547 } 548 549 b, err := io.ReadAll(res.Body) 550 helper.Must(err) 551 552 var result []string 553 helper.Must(json.Unmarshal(b, &result)) 554 555 if diff := cmp.Diff(result, []string{"value1", "", "default_value_3", "value4", "value5"}); diff != "" { 556 t.Error(diff) 557 } 558 } 559 560 func TestHTTPServer_XFHHeader(t *testing.T) { 561 client := newClient() 562 563 env.SetTestOsEnviron(func() []string { 564 return []string{"COUPER_XFH=true"} 565 }) 566 defer env.SetTestOsEnviron(os.Environ) 567 568 confPath := path.Join("testdata/integration", "files/02_couper.hcl") 569 shutdown, logHook := newCouper(confPath, test.New(t)) 570 defer shutdown() 571 572 helper := test.New(t) 573 logHook.Reset() 574 575 req, err := http.NewRequest(http.MethodGet, "http://example.com:9898/b", nil) 576 helper.Must(err) 577 578 req.Host = "example.com" 579 req.Header.Set("X-Forwarded-Host", "example.com.") 580 res, err := client.Do(req) 581 helper.Must(err) 582 583 resBytes, err := io.ReadAll(res.Body) 584 helper.Must(err) 585 586 _ = res.Body.Close() 587 588 if string(resBytes) != `<html lang="en">index B</html>` { 589 t.Errorf("%s", resBytes) 590 } 591 592 entry := logHook.LastEntry() 593 if entry == nil { 594 t.Error("Expected a log entry, got nothing") 595 } else if entry.Data["server"] != "multi-files-host2" { 596 t.Errorf("Expected 'multi-files-host2', got: %s", entry.Data["server"]) 597 } else if entry.Data["url"] != "http://example.com:9898/b" { 598 t.Errorf("Expected 'http://example.com:9898/b', got: %s", entry.Data["url"]) 599 } 600 } 601 602 func TestHTTPServer_ProxyFromEnv(t *testing.T) { 603 helper := test.New(t) 604 605 seen := make(chan struct{}) 606 origin := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 607 rw.WriteHeader(http.StatusNoContent) 608 go func() { 609 seen <- struct{}{} 610 }() 611 })) 612 ln, err := net.Listen("tcp4", testProxyAddr[7:]) 613 helper.Must(err) 614 origin.Listener = ln 615 origin.Start() 616 defer func() { 617 origin.Close() 618 ln.Close() 619 time.Sleep(time.Second) 620 }() 621 622 confPath := path.Join("testdata/integration", "api/01_couper.hcl") 623 shutdown, _ := newCouper(confPath, test.New(t)) 624 defer shutdown() 625 626 req, err := http.NewRequest(http.MethodGet, "http://anyserver:8080/v1/proxy", nil) 627 helper.Must(err) 628 629 _, err = newClient().Do(req) 630 helper.Must(err) 631 632 timer := time.NewTimer(time.Second) 633 select { 634 case <-timer.C: 635 t.Error("Missing proxy call") 636 case <-seen: 637 } 638 } 639 640 func TestHTTPServer_Gzip(t *testing.T) { 641 client := newClient() 642 643 confPath := path.Join("testdata/integration", "files/03_gzip.hcl") 644 shutdown, _ := newCouper(confPath, test.New(t)) 645 defer shutdown() 646 647 type testCase struct { 648 name string 649 headerAcceptEncoding string 650 path string 651 expectGzipResponse bool 652 } 653 654 for _, tc := range []testCase{ 655 {"with mixed header AE gzip", "br, gzip", "/index.html", true}, 656 {"with header AE gzip", "gzip", "/index.html", true}, 657 {"with header AE and without gzip", "deflate", "/index.html", false}, 658 {"with header AE and space", " ", "/index.html", false}, 659 } { 660 t.Run(tc.name, func(subT *testing.T) { 661 helper := test.New(subT) 662 663 req, err := http.NewRequest(http.MethodGet, "http://example.org:9898"+tc.path, nil) 664 helper.Must(err) 665 666 if tc.headerAcceptEncoding != "" { 667 req.Header.Set("Accept-Encoding", tc.headerAcceptEncoding) 668 } 669 670 res, err := client.Do(req) 671 helper.Must(err) 672 673 var body io.Reader 674 body = res.Body 675 676 if !tc.expectGzipResponse { 677 if val := res.Header.Get("Content-Encoding"); val != "" { 678 subT.Errorf("Expected no header with key Content-Encoding, got value: %s", val) 679 } 680 } else { 681 if ce := res.Header.Get("Content-Encoding"); ce != "gzip" { 682 subT.Errorf("Expected Content-Encoding header value: %q, got: %q", "gzip", ce) 683 } 684 685 body, err = gzip.NewReader(res.Body) 686 helper.Must(err) 687 } 688 689 if vr := res.Header.Get("Vary"); vr != "Accept-Encoding" { 690 subT.Errorf("Expected Accept-Encoding header value %q, got: %q", "Vary", vr) 691 } 692 693 resBytes, err := io.ReadAll(body) 694 helper.Must(err) 695 696 srcBytes, err := os.ReadFile(filepath.Join(testWorkingDir, "testdata/integration/files/htdocs_c_gzip"+tc.path)) 697 helper.Must(err) 698 699 if !bytes.Equal(resBytes, srcBytes) { 700 subT.Errorf("Want:\n%s\nGot:\n%s", string(srcBytes), string(resBytes)) 701 } 702 }) 703 } 704 } 705 706 func TestHTTPServer_QueryParams(t *testing.T) { 707 client := newClient() 708 709 const confPath = "testdata/integration/endpoint_eval/" 710 711 type expectation struct { 712 Query url.Values 713 Path string 714 } 715 716 type testCase struct { 717 file string 718 query string 719 exp expectation 720 } 721 722 for _, tc := range []testCase{ 723 {"04_couper.hcl", "a=b%20c&aeb_del=1&ae_del=1&CaseIns=1&caseIns=1&def_del=1&xyz=123", expectation{ 724 Query: url.Values{ 725 "a": []string{"b c"}, 726 "ae_a_and_b": []string{"A&B", "A&B"}, 727 "ae_empty": []string{"", ""}, 728 "ae_multi": []string{"str1", "str2", "str3", "str4"}, 729 "ae_string": []string{"str", "str"}, 730 "ae": []string{"ae", "ae"}, 731 "aeb_a_and_b": []string{"A&B", "A&B"}, 732 "aeb_empty": []string{"", ""}, 733 "aeb_multi": []string{"str1", "str2", "str3", "str4"}, 734 "aeb_string": []string{"str", "str"}, 735 "aeb": []string{"aeb", "aeb"}, 736 "caseIns": []string{"1"}, 737 "def_del": []string{"1"}, 738 "xxx": []string{"aaa", "bbb"}, 739 }, 740 Path: "/", 741 }}, 742 {"05_couper.hcl", "", expectation{ 743 Query: url.Values{ 744 "ae": []string{"ae"}, 745 "def": []string{"def"}, 746 }, 747 Path: "/xxx", 748 }}, 749 {"06_couper.hcl", "", expectation{ 750 Query: url.Values{ 751 "ae": []string{"ae"}, 752 "def": []string{"def"}, 753 }, 754 Path: "/zzz", 755 }}, 756 {"07_couper.hcl", "", expectation{ 757 Query: url.Values{ 758 "ae": []string{"ae"}, 759 "def": []string{"def"}, 760 }, 761 Path: "/xxx", 762 }}, 763 {"09_couper.hcl", "", expectation{ 764 Query: url.Values{ 765 "test": []string{"pest"}, 766 }, 767 Path: "/", 768 }}, 769 } { 770 t.Run("_"+tc.query, func(subT *testing.T) { 771 helper := test.New(subT) 772 773 shutdown, _ := newCouper(path.Join(confPath, tc.file), helper) 774 defer shutdown() 775 776 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?"+tc.query, nil) 777 helper.Must(err) 778 779 req.Header.Set("ae", "ae") 780 req.Header.Set("aeb", "aeb") 781 req.Header.Set("def", "def") 782 req.Header.Set("xyz", "xyz") 783 784 res, err := client.Do(req) 785 helper.Must(err) 786 787 resBytes, err := io.ReadAll(res.Body) 788 helper.Must(err) 789 790 _ = res.Body.Close() 791 792 var jsonResult expectation 793 err = json.Unmarshal(resBytes, &jsonResult) 794 if err != nil { 795 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 796 } 797 798 if !reflect.DeepEqual(jsonResult, tc.exp) { 799 subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes)) 800 } 801 }) 802 } 803 } 804 805 func TestHTTPServer_PathPrefix(t *testing.T) { 806 client := newClient() 807 808 type expectation struct { 809 Path string 810 } 811 812 type testCase struct { 813 path string 814 exp expectation 815 } 816 817 for _, tc := range []testCase{ 818 {"/v1", expectation{ 819 Path: "/xxx/xxx/v1", 820 }}, 821 {"/v1/vvv/foo", expectation{ 822 Path: "/xxx/xxx/api/foo", 823 }}, 824 {"/v2/yyy", expectation{ 825 Path: "/v2/yyy", 826 }}, 827 {"/v3/zzz", expectation{ 828 Path: "/zzz/v3/zzz", 829 }}, 830 } { 831 t.Run("_"+tc.path, func(subT *testing.T) { 832 helper := test.New(subT) 833 834 shutdown, _ := newCouper("testdata/integration/api/06_couper.hcl", helper) 835 defer shutdown() 836 837 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 838 helper.Must(err) 839 840 // Test dynamic values in conf 841 if strings.HasPrefix(tc.exp.Path, "/xxx") { 842 req.Header.Set("X-Val", "xxx") 843 } 844 845 res, err := client.Do(req) 846 helper.Must(err) 847 848 resBytes, err := io.ReadAll(res.Body) 849 helper.Must(err) 850 851 _ = res.Body.Close() 852 853 var jsonResult expectation 854 err = json.Unmarshal(resBytes, &jsonResult) 855 if err != nil { 856 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 857 } 858 859 if !reflect.DeepEqual(jsonResult, tc.exp) { 860 subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes)) 861 } 862 }) 863 } 864 } 865 866 func TestHTTPServer_BackendLogPath(t *testing.T) { 867 client := newClient() 868 helper := test.New(t) 869 870 shutdown, hook := newCouper("testdata/integration/api/07_couper.hcl", helper) 871 defer shutdown() 872 873 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil) 874 helper.Must(err) 875 876 hook.Reset() 877 _, err = client.Do(req) 878 helper.Must(err) 879 880 if p := hook.AllEntries()[0].Data["request"].(logging.Fields)["path"]; p != "/path?query" { 881 t.Errorf("Unexpected path given: %s", p) 882 } 883 } 884 885 func TestHTTPServer_BackendLogRequestProto(t *testing.T) { 886 client := newClient() 887 helper := test.New(t) 888 889 shutdown, hook := newCouper("testdata/integration/api/15_couper.hcl", helper) 890 defer shutdown() 891 892 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil) 893 helper.Must(err) 894 895 hook.Reset() 896 _, err = client.Do(req) 897 helper.Must(err) 898 899 var backendLogsSeen int 900 for _, entry := range hook.AllEntries() { 901 if entry.Data["type"] == "couper_access" { 902 continue 903 } 904 905 backendLogsSeen++ 906 907 if p := entry.Data["request"].(logging.Fields)["proto"]; p != "http" { 908 t.Errorf("want proto http, got: %q", p) 909 } 910 } 911 912 if backendLogsSeen != 2 { 913 t.Error("expected two backend request logs") 914 } 915 } 916 917 func TestHTTPServer_PathInvalidFragment(t *testing.T) { 918 client := newClient() 919 helper := test.New(t) 920 921 shutdown, hook := newCouper("testdata/integration/api/09_couper.hcl", helper) 922 defer shutdown() 923 924 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil) 925 helper.Must(err) 926 927 hook.Reset() 928 _, err = client.Do(req) 929 helper.Must(err) 930 931 if m := hook.AllEntries()[0].Message; m != "configuration error: path attribute: invalid fragment found in \"/path#xxx\"" { 932 t.Errorf("Unexpected message given: %s", m) 933 } 934 } 935 936 func TestHTTPServer_PathInvalidQuery(t *testing.T) { 937 client := newClient() 938 helper := test.New(t) 939 940 shutdown, hook := newCouper("testdata/integration/api/10_couper.hcl", helper) 941 defer shutdown() 942 943 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil) 944 helper.Must(err) 945 946 hook.Reset() 947 _, err = client.Do(req) 948 helper.Must(err) 949 950 if m := hook.AllEntries()[0].Message; m != "configuration error: path attribute: invalid query string found in \"/path?xxx\"" { 951 t.Errorf("Unexpected message given: %s", m) 952 } 953 } 954 955 func TestHTTPServer_PathPrefixInvalidFragment(t *testing.T) { 956 client := newClient() 957 helper := test.New(t) 958 959 shutdown, hook := newCouper("testdata/integration/api/11_couper.hcl", helper) 960 defer shutdown() 961 962 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil) 963 helper.Must(err) 964 965 hook.Reset() 966 _, err = client.Do(req) 967 helper.Must(err) 968 969 if m := hook.AllEntries()[0].Message; m != "configuration error: path_prefix attribute: invalid fragment found in \"/path#xxx\"" { 970 t.Errorf("Unexpected message given: %s", m) 971 } 972 } 973 974 func TestHTTPServer_PathPrefixInvalidQuery(t *testing.T) { 975 client := newClient() 976 helper := test.New(t) 977 978 shutdown, hook := newCouper("testdata/integration/api/12_couper.hcl", helper) 979 defer shutdown() 980 981 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?query#fragment", nil) 982 helper.Must(err) 983 984 hook.Reset() 985 _, err = client.Do(req) 986 helper.Must(err) 987 988 if m := hook.AllEntries()[0].Message; m != "configuration error: path_prefix attribute: invalid query string found in \"/path?xxx\"" { 989 t.Errorf("Unexpected message given: %s", m) 990 } 991 } 992 993 func TestHTTPServer_RequestHeaders(t *testing.T) { 994 client := newClient() 995 996 const confPath = "testdata/integration/endpoint_eval/" 997 998 type expectation struct { 999 Headers http.Header 1000 } 1001 1002 type testCase struct { 1003 file string 1004 query string 1005 exp expectation 1006 } 1007 1008 for _, tc := range []testCase{ 1009 {"12_couper.hcl", "ae=ae&aeb=aeb&def=def&xyz=xyz", expectation{ 1010 Headers: http.Header{ 1011 "Aeb": []string{"aeb", "aeb"}, 1012 "Aeb_a_and_b": []string{"A&B", "A&B"}, 1013 "Aeb_empty": []string{"", ""}, 1014 "Aeb_multi": []string{"str1", "str2", "str3", "str4"}, 1015 "Aeb_string": []string{"str", "str"}, 1016 "Xxx": []string{"aaa", "bbb"}, 1017 }, 1018 }}, 1019 } { 1020 t.Run("_"+tc.query, func(subT *testing.T) { 1021 helper := test.New(subT) 1022 shutdown, _ := newCouper(path.Join(confPath, tc.file), helper) 1023 defer shutdown() 1024 1025 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?"+tc.query, nil) 1026 helper.Must(err) 1027 1028 res, err := client.Do(req) 1029 helper.Must(err) 1030 1031 if r1 := res.Header.Get("Remove-Me-1"); r1 != "r1" { 1032 subT.Errorf("Missing or invalid header Remove-Me-1: %s", r1) 1033 } 1034 if r2 := res.Header.Get("Remove-Me-2"); r2 != "" { 1035 subT.Errorf("Unexpected header %s", r2) 1036 } 1037 1038 if s2 := res.Header.Get("Set-Me-2"); s2 != "s2" { 1039 subT.Errorf("Missing or invalid header Set-Me-2: %s", s2) 1040 } 1041 1042 if a2 := res.Header.Get("Add-Me-2"); a2 != "a2" { 1043 subT.Errorf("Missing or invalid header Add-Me-2: %s", a2) 1044 } 1045 1046 resBytes, err := io.ReadAll(res.Body) 1047 helper.Must(err) 1048 1049 _ = res.Body.Close() 1050 1051 var jsonResult expectation 1052 err = json.Unmarshal(resBytes, &jsonResult) 1053 if err != nil { 1054 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1055 } 1056 1057 jsonResult.Headers.Del("User-Agent") 1058 jsonResult.Headers.Del("X-Forwarded-For") 1059 jsonResult.Headers.Del("Couper-Request-Id") 1060 1061 if !reflect.DeepEqual(jsonResult, tc.exp) { 1062 subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes)) 1063 } 1064 }) 1065 } 1066 } 1067 1068 func TestHTTPServer_LogFields(t *testing.T) { 1069 client := newClient() 1070 conf := "testdata/integration/endpoint_eval/10_couper.hcl" 1071 1072 helper := test.New(t) 1073 shutdown, logHook := newCouper(conf, helper) 1074 defer shutdown() 1075 1076 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil) 1077 helper.Must(err) 1078 1079 res, err := client.Do(req) 1080 helper.Must(err) 1081 1082 entries := logHook.AllEntries() 1083 if l := len(entries); l != 2 { 1084 t.Fatalf("Unexpected number of log lines: %d", l) 1085 } 1086 1087 resBytes, err := io.ReadAll(res.Body) 1088 helper.Must(err) 1089 helper.Must(res.Body.Close()) 1090 1091 backendLog := entries[0] 1092 accessLog := entries[1] 1093 1094 if tp, ok := backendLog.Data["type"]; !ok || tp != "couper_backend" { 1095 t.Fatalf("Unexpected log type: %s", tp) 1096 } 1097 if tp, ok := accessLog.Data["type"]; !ok || tp != "couper_access" { 1098 t.Fatalf("Unexpected log type: %s", tp) 1099 } 1100 1101 if u, ok := backendLog.Data["url"]; !ok || u == "" { 1102 t.Fatalf("Unexpected URL: %s", u) 1103 } 1104 if u, ok := accessLog.Data["url"]; !ok || u == "" { 1105 t.Fatalf("Unexpected URL: %s", u) 1106 } 1107 1108 if b, ok := backendLog.Data["backend"]; !ok || b != "anything" { 1109 t.Fatalf("Unexpected backend name: %s", b) 1110 } 1111 if e, ok := accessLog.Data["endpoint"]; !ok || e != "/" { 1112 t.Fatalf("Unexpected endpoint: %s", e) 1113 } 1114 1115 if b, ok := accessLog.Data["response"].(logging.Fields)["bytes"]; !ok || b != len(resBytes) { 1116 t.Fatalf("Unexpected number of bytes: %d\npayload: %s", b, string(resBytes)) 1117 } 1118 } 1119 1120 func TestHTTPServer_QueryEncoding(t *testing.T) { 1121 client := newClient() 1122 1123 conf := "testdata/integration/endpoint_eval/10_couper.hcl" 1124 1125 type expectation struct { 1126 RawQuery string 1127 } 1128 1129 helper := test.New(t) 1130 shutdown, _ := newCouper(conf, helper) 1131 defer shutdown() 1132 1133 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080?a=a%20a&x=x+x", nil) 1134 helper.Must(err) 1135 1136 res, err := client.Do(req) 1137 helper.Must(err) 1138 1139 resBytes, err := io.ReadAll(res.Body) 1140 helper.Must(err) 1141 1142 _ = res.Body.Close() 1143 1144 var jsonResult expectation 1145 err = json.Unmarshal(resBytes, &jsonResult) 1146 if err != nil { 1147 t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1148 } 1149 1150 exp := expectation{RawQuery: "a=a%20a&space=a%20b%2Bc&x=x%2Bx"} 1151 if !reflect.DeepEqual(jsonResult, exp) { 1152 t.Errorf("\nwant: \n%#v\ngot: \n%#v", exp, jsonResult) 1153 } 1154 } 1155 1156 func TestHTTPServer_Backends(t *testing.T) { 1157 client := newClient() 1158 1159 configPath := "testdata/integration/config/02_couper.hcl" 1160 1161 helper := test.New(t) 1162 shutdown, _ := newCouper(configPath, helper) 1163 defer shutdown() 1164 1165 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil) 1166 helper.Must(err) 1167 1168 res, err := client.Do(req) 1169 helper.Must(err) 1170 1171 exp := []string{"1", "4"} 1172 if !reflect.DeepEqual(res.Header.Values("Foo"), exp) { 1173 t.Errorf("\nwant: \n%#v\ngot: \n%#v", exp, res.Header.Values("Foo")) 1174 } 1175 } 1176 1177 func TestHTTPServer_Backends_Reference(t *testing.T) { 1178 client := newClient() 1179 1180 configPath := "testdata/integration/config/04_couper.hcl" 1181 1182 helper := test.New(t) 1183 shutdown, _ := newCouper(configPath, helper) 1184 defer shutdown() 1185 1186 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/", nil) 1187 helper.Must(err) 1188 1189 res, err := client.Do(req) 1190 helper.Must(err) 1191 1192 if res.Header.Get("proxy") != "a" || res.Header.Get("request") != "b" { 1193 t.Errorf("Expected proxy:a and request:b header values, got: %v", res.Header) 1194 } 1195 } 1196 1197 func TestHTTPServer_Backends_Reference_BasicAuth(t *testing.T) { 1198 client := newClient() 1199 1200 configPath := "testdata/integration/config/13_couper.hcl" 1201 1202 helper := test.New(t) 1203 shutdown, _ := newCouper(configPath, helper) 1204 defer shutdown() 1205 1206 type testcase struct { 1207 path string 1208 wantAuth bool 1209 } 1210 1211 for _, tc := range []testcase{ 1212 {"/", false}, 1213 {"/granted", true}, 1214 } { 1215 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 1216 helper.Must(err) 1217 1218 res, err := client.Do(req) 1219 helper.Must(err) 1220 1221 b, err := io.ReadAll(res.Body) 1222 helper.Must(err) 1223 1224 helper.Must(res.Body.Close()) 1225 1226 type result struct { 1227 Headers http.Header 1228 } 1229 r := result{} 1230 helper.Must(json.Unmarshal(b, &r)) 1231 1232 if tc.wantAuth && !strings.HasPrefix(r.Headers.Get("Authorization"), "Basic ") { 1233 t.Error("expected Authorization header value") 1234 } 1235 } 1236 } 1237 1238 func TestHTTPServer_Backends_Reference_PathPrefix(t *testing.T) { 1239 client := newClient() 1240 1241 configPath := "testdata/integration/config/12_couper.hcl" 1242 1243 helper := test.New(t) 1244 shutdown, _ := newCouper(configPath, helper) 1245 defer shutdown() 1246 1247 type testcase struct { 1248 path string 1249 wantPath string 1250 wantStatus int 1251 } 1252 1253 for _, tc := range []testcase{ 1254 {"/", "/anything", http.StatusOK}, 1255 {"/prefixed", "/my-prefix/anything", http.StatusNotFound}, 1256 } { 1257 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 1258 helper.Must(err) 1259 1260 res, err := client.Do(req) 1261 helper.Must(err) 1262 1263 type result struct { 1264 Path string 1265 } 1266 1267 b, err := io.ReadAll(res.Body) 1268 helper.Must(err) 1269 1270 helper.Must(res.Body.Close()) 1271 1272 r := result{} 1273 helper.Must(json.Unmarshal(b, &r)) 1274 1275 if res.StatusCode != tc.wantStatus { 1276 t.Errorf("expected status: %d, got %d", tc.wantStatus, res.StatusCode) 1277 } 1278 1279 if r.Path != tc.wantPath { 1280 t.Errorf("expected path: %q, got: %q", tc.wantPath, r.Path) 1281 } 1282 } 1283 } 1284 1285 func TestHTTPServer_OriginVsURL(t *testing.T) { 1286 client := newClient() 1287 1288 configPath := "testdata/integration/url/" 1289 1290 type expectation struct { 1291 Path string 1292 Query url.Values 1293 } 1294 1295 type testCase struct { 1296 file string 1297 exp expectation 1298 } 1299 1300 for _, tc := range []testCase{ 1301 {"01_couper.hcl", expectation{ 1302 Path: "/anything", 1303 Query: url.Values{ 1304 "x": []string{"y"}, 1305 }, 1306 }}, 1307 {"02_couper.hcl", expectation{ 1308 Path: "/anything", 1309 Query: url.Values{ 1310 "a": []string{"A"}, 1311 }, 1312 }}, 1313 {"03_couper.hcl", expectation{ 1314 Path: "/anything", 1315 Query: url.Values{ 1316 "a": []string{"A"}, 1317 "x": []string{"y"}, 1318 }, 1319 }}, 1320 {"04_couper.hcl", expectation{ 1321 Path: "/anything", 1322 Query: url.Values{ 1323 "a": []string{"A"}, 1324 "x": []string{"y"}, 1325 }, 1326 }}, 1327 {"05_couper.hcl", expectation{ 1328 Path: "/anything", 1329 Query: url.Values{ 1330 "a": []string{"A"}, 1331 "x": []string{"y"}, 1332 }, 1333 }}, 1334 {"06_couper.hcl", expectation{ 1335 Path: "/anything", 1336 Query: url.Values{ 1337 "a": []string{"A"}, 1338 "x": []string{"y"}, 1339 }, 1340 }}, 1341 } { 1342 t.Run("File "+tc.file, func(subT *testing.T) { 1343 helper := test.New(subT) 1344 1345 shutdown, _ := newCouper(path.Join(configPath, tc.file), helper) 1346 defer shutdown() 1347 1348 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil) 1349 helper.Must(err) 1350 1351 res, err := client.Do(req) 1352 helper.Must(err) 1353 1354 resBytes, err := io.ReadAll(res.Body) 1355 helper.Must(err) 1356 res.Body.Close() 1357 1358 var jsonResult expectation 1359 err = json.Unmarshal(resBytes, &jsonResult) 1360 if err != nil { 1361 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1362 } 1363 1364 if !reflect.DeepEqual(jsonResult, tc.exp) { 1365 subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult) 1366 } 1367 }) 1368 } 1369 } 1370 1371 func TestHTTPServer_TrailingSlash(t *testing.T) { 1372 client := newClient() 1373 1374 conf := "testdata/integration/endpoint_eval/11_couper.hcl" 1375 1376 type expectation struct { 1377 Path string 1378 } 1379 1380 type testCase struct { 1381 path string 1382 exp expectation 1383 } 1384 1385 for _, tc := range []testCase{ 1386 {"/path", expectation{ 1387 Path: "/path", 1388 }}, 1389 {"/path/", expectation{ 1390 Path: "/path/", 1391 }}, 1392 } { 1393 t.Run("TrailingSlash "+tc.path, func(subT *testing.T) { 1394 helper := test.New(subT) 1395 shutdown, _ := newCouper(conf, helper) 1396 defer shutdown() 1397 1398 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 1399 helper.Must(err) 1400 1401 res, err := client.Do(req) 1402 helper.Must(err) 1403 1404 resBytes, err := io.ReadAll(res.Body) 1405 helper.Must(err) 1406 1407 _ = res.Body.Close() 1408 1409 var jsonResult expectation 1410 err = json.Unmarshal(resBytes, &jsonResult) 1411 if err != nil { 1412 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1413 } 1414 1415 if !reflect.DeepEqual(jsonResult, tc.exp) { 1416 subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult) 1417 } 1418 }) 1419 } 1420 } 1421 1422 func TestHTTPServer_DynamicRequest(t *testing.T) { 1423 client := newClient() 1424 1425 configFile := "testdata/integration/endpoint_eval/13_couper.hcl" 1426 shutdown, _ := newCouper(configFile, test.New(t)) 1427 defer shutdown() 1428 1429 type expectation struct { 1430 Body string 1431 Headers http.Header 1432 Method string 1433 Path string 1434 Query url.Values 1435 } 1436 1437 type testCase struct { 1438 exp expectation 1439 } 1440 1441 for _, tc := range []testCase{ 1442 {expectation{ 1443 Body: "body", 1444 Method: "PUT", 1445 Path: "/anything", 1446 Query: url.Values{ 1447 "q": []string{"query"}, 1448 }, 1449 Headers: http.Header{ 1450 "Content-Length": []string{"4"}, 1451 "Content-Type": []string{"text/plain"}, 1452 "Test": []string{"header"}, 1453 }, 1454 }}, 1455 } { 1456 t.Run("Dynamic request", func(subT *testing.T) { 1457 helper := test.New(subT) 1458 1459 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/?method=put", nil) 1460 helper.Must(err) 1461 1462 req.Header.Set("Body", "body") 1463 req.Header.Set("Query", "query") 1464 req.Header.Set("Test", "header") 1465 1466 res, err := client.Do(req) 1467 helper.Must(err) 1468 1469 resBytes, err := io.ReadAll(res.Body) 1470 helper.Must(err) 1471 res.Body.Close() 1472 1473 var jsonResult expectation 1474 err = json.Unmarshal(resBytes, &jsonResult) 1475 if err != nil { 1476 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1477 } 1478 1479 if !reflect.DeepEqual(jsonResult, tc.exp) { 1480 subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult) 1481 } 1482 }) 1483 } 1484 } 1485 1486 func TestHTTPServer_request_bodies(t *testing.T) { 1487 client := newClient() 1488 1489 configFile := "testdata/integration/endpoint_eval/14_couper.hcl" 1490 shutdown, _ := newCouper(configFile, test.New(t)) 1491 defer shutdown() 1492 1493 type expectation struct { 1494 Body string 1495 Args url.Values 1496 Headers http.Header 1497 Method string 1498 } 1499 1500 type testCase struct { 1501 path string 1502 clientPayload string 1503 clientContentType string 1504 exp expectation 1505 } 1506 1507 for _, tc := range []testCase{ 1508 { 1509 "/request/body", 1510 "", 1511 "", 1512 expectation{ 1513 Body: "foo", 1514 Args: url.Values{}, 1515 Method: "POST", 1516 Headers: http.Header{ 1517 "Content-Length": []string{"3"}, 1518 "Content-Type": []string{"text/plain"}, 1519 }, 1520 }, 1521 }, 1522 { 1523 "/request/body/ct", 1524 "", 1525 "", 1526 expectation{ 1527 Body: "foo", 1528 Args: url.Values{}, 1529 Method: "POST", 1530 Headers: http.Header{ 1531 "Content-Length": []string{"3"}, 1532 "Content-Type": []string{"application/foo"}, 1533 }, 1534 }, 1535 }, 1536 { 1537 "/request/json_body/null", 1538 "", 1539 "", 1540 expectation{ 1541 Body: "null", 1542 Args: url.Values{}, 1543 Method: "POST", 1544 Headers: http.Header{ 1545 "Content-Length": []string{"4"}, 1546 "Content-Type": []string{"application/json"}, 1547 }, 1548 }, 1549 }, 1550 { 1551 "/request/json_body/boolean", 1552 "", 1553 "", 1554 expectation{ 1555 Body: "true", 1556 Args: url.Values{}, 1557 Method: "POST", 1558 Headers: http.Header{ 1559 "Content-Length": []string{"4"}, 1560 "Content-Type": []string{"application/json"}, 1561 }, 1562 }, 1563 }, 1564 { 1565 "/request/json_body/boolean/ct", 1566 "", 1567 "", 1568 expectation{ 1569 Body: "true", 1570 Args: url.Values{}, 1571 Method: "POST", 1572 Headers: http.Header{ 1573 "Content-Length": []string{"4"}, 1574 "Content-Type": []string{"application/foo+json"}, 1575 }, 1576 }, 1577 }, 1578 { 1579 "/request/json_body/number", 1580 "", 1581 "", 1582 expectation{ 1583 Body: "1.2", 1584 Args: url.Values{}, 1585 Method: "POST", 1586 Headers: http.Header{ 1587 "Content-Length": []string{"3"}, 1588 "Content-Type": []string{"application/json"}, 1589 }, 1590 }, 1591 }, 1592 { 1593 "/request/json_body/string", 1594 "", 1595 "", 1596 expectation{ 1597 Body: `"föö"`, 1598 Args: url.Values{}, 1599 Method: "POST", 1600 Headers: http.Header{ 1601 "Content-Length": []string{"7"}, 1602 "Content-Type": []string{"application/json"}, 1603 }, 1604 }, 1605 }, 1606 { 1607 "/request/json_body/object", 1608 "", 1609 "", 1610 expectation{ 1611 Body: `{"url":"http://...?foo&bar"}`, 1612 Args: url.Values{}, 1613 Method: "POST", 1614 Headers: http.Header{ 1615 "Content-Length": []string{"28"}, 1616 "Content-Type": []string{"application/json"}, 1617 }, 1618 }, 1619 }, 1620 { 1621 "/request/json_body/object/html", 1622 "", 1623 "", 1624 expectation{ 1625 Body: `{"foo":"<p>bar</p>"}`, 1626 Args: url.Values{}, 1627 Method: "POST", 1628 Headers: http.Header{ 1629 "Content-Length": []string{"20"}, 1630 "Content-Type": []string{"application/json"}, 1631 }, 1632 }, 1633 }, 1634 { 1635 "/request/json_body/array", 1636 "", 1637 "", 1638 expectation{ 1639 Body: "[0,1,2]", 1640 Args: url.Values{}, 1641 Method: "POST", 1642 Headers: http.Header{ 1643 "Content-Length": []string{"7"}, 1644 "Content-Type": []string{"application/json"}, 1645 }, 1646 }, 1647 }, 1648 { 1649 "/request/json_body/dyn", 1650 "true", 1651 "application/json", 1652 expectation{ 1653 Body: "true", 1654 Args: url.Values{}, 1655 Method: "POST", 1656 Headers: http.Header{ 1657 "Content-Length": []string{"4"}, 1658 "Content-Type": []string{"application/json"}, 1659 }, 1660 }, 1661 }, 1662 { 1663 "/request/json_body/dyn", 1664 "1.23", 1665 "application/json", 1666 expectation{ 1667 Body: "1.23", 1668 Args: url.Values{}, 1669 Method: "POST", 1670 Headers: http.Header{ 1671 "Content-Length": []string{"4"}, 1672 "Content-Type": []string{"application/json"}, 1673 }, 1674 }, 1675 }, 1676 { 1677 "/request/json_body/dyn", 1678 "\"ab\"", 1679 "application/json", 1680 expectation{ 1681 Body: "\"ab\"", 1682 Args: url.Values{}, 1683 Method: "POST", 1684 Headers: http.Header{ 1685 "Content-Length": []string{"4"}, 1686 "Content-Type": []string{"application/json"}, 1687 }, 1688 }, 1689 }, 1690 { 1691 "/request/json_body/dyn", 1692 "{\"a\":3,\"b\":[]}", 1693 "application/json", 1694 expectation{ 1695 Body: "{\"a\":3,\"b\":[]}", 1696 Args: url.Values{}, 1697 Method: "POST", 1698 Headers: http.Header{ 1699 "Content-Length": []string{"14"}, 1700 "Content-Type": []string{"application/json"}, 1701 }, 1702 }, 1703 }, 1704 { 1705 "/request/json_body/dyn", 1706 "[0,1]", 1707 "application/json", 1708 expectation{ 1709 Body: "[0,1]", 1710 Args: url.Values{}, 1711 Method: "POST", 1712 Headers: http.Header{ 1713 "Content-Length": []string{"5"}, 1714 "Content-Type": []string{"application/json"}, 1715 }, 1716 }, 1717 }, 1718 { 1719 "/request/form_body", 1720 "", 1721 "", 1722 expectation{ 1723 Body: "", 1724 Args: url.Values{ 1725 "foo": []string{"ab c"}, 1726 "bar": []string{",:/"}, 1727 }, 1728 Method: "POST", 1729 Headers: http.Header{ 1730 "Content-Length": []string{"22"}, 1731 "Content-Type": []string{"application/x-www-form-urlencoded"}, 1732 }, 1733 }, 1734 }, 1735 { 1736 "/request/form_body/ct", 1737 "", 1738 "", 1739 expectation{ 1740 Body: "bar=%2C%3A%2F&foo=ab+c", 1741 Args: url.Values{}, 1742 Method: "POST", 1743 Headers: http.Header{ 1744 "Content-Length": []string{"22"}, 1745 "Content-Type": []string{"application/my-form-urlencoded"}, 1746 }, 1747 }, 1748 }, 1749 { 1750 "/request/form_body/dyn", 1751 "bar=%2C&foo=a", 1752 "application/x-www-form-urlencoded", 1753 expectation{ 1754 Body: "", 1755 Args: url.Values{ 1756 "foo": []string{"a"}, 1757 "bar": []string{","}, 1758 }, 1759 Method: "POST", 1760 Headers: http.Header{ 1761 "Content-Length": []string{"13"}, 1762 "Content-Type": []string{"application/x-www-form-urlencoded"}, 1763 }, 1764 }, 1765 }, 1766 } { 1767 t.Run(tc.path, func(subT *testing.T) { 1768 helper := test.New(subT) 1769 1770 req, err := http.NewRequest(http.MethodPost, "http://example.com:8080"+tc.path, strings.NewReader(tc.clientPayload)) 1771 helper.Must(err) 1772 1773 if tc.clientContentType != "" { 1774 req.Header.Set("Content-Type", tc.clientContentType) 1775 } 1776 1777 res, err := client.Do(req) 1778 helper.Must(err) 1779 1780 resBytes, err := io.ReadAll(res.Body) 1781 helper.Must(err) 1782 res.Body.Close() 1783 1784 var jsonResult expectation 1785 err = json.Unmarshal(resBytes, &jsonResult) 1786 if err != nil { 1787 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1788 } 1789 1790 if !reflect.DeepEqual(jsonResult, tc.exp) { 1791 subT.Errorf("\nwant: \n%#v\ngot: \n%#v", tc.exp, jsonResult) 1792 } 1793 }) 1794 } 1795 } 1796 1797 func TestHTTPServer_response_bodies(t *testing.T) { 1798 client := newClient() 1799 1800 configFile := "testdata/integration/endpoint_eval/14_couper.hcl" 1801 shutdown, _ := newCouper(configFile, test.New(t)) 1802 defer shutdown() 1803 1804 type expectation struct { 1805 Body string 1806 ContentType string 1807 } 1808 1809 type testCase struct { 1810 path string 1811 exp expectation 1812 } 1813 1814 for _, tc := range []testCase{ 1815 { 1816 "/response/body", 1817 expectation{ 1818 Body: "foo", 1819 ContentType: "text/plain", 1820 }, 1821 }, 1822 { 1823 "/response/body/ct", 1824 expectation{ 1825 Body: "foo", 1826 ContentType: "application/foo", 1827 }, 1828 }, 1829 { 1830 "/response/json_body/null", 1831 expectation{ 1832 Body: "null", 1833 ContentType: "application/json", 1834 }, 1835 }, 1836 { 1837 "/response/json_body/boolean", 1838 expectation{ 1839 Body: "true", 1840 ContentType: "application/json", 1841 }, 1842 }, 1843 { 1844 "/response/json_body/boolean/ct", 1845 expectation{ 1846 Body: "true", 1847 ContentType: "application/foo+json", 1848 }, 1849 }, 1850 { 1851 "/response/json_body/number", 1852 expectation{ 1853 Body: "1.2", 1854 ContentType: "application/json", 1855 }, 1856 }, 1857 { 1858 "/response/json_body/string", 1859 expectation{ 1860 Body: `"foo"`, 1861 ContentType: "application/json", 1862 }, 1863 }, 1864 { 1865 "/response/json_body/object", 1866 expectation{ 1867 Body: `{"foo":"bar"}`, 1868 ContentType: "application/json", 1869 }, 1870 }, 1871 { 1872 "/response/json_body/object/html", 1873 expectation{ 1874 Body: `{"foo":"<p>bar</p>"}`, 1875 ContentType: "application/json", 1876 }, 1877 }, 1878 { 1879 "/response/json_body/array", 1880 expectation{ 1881 Body: "[0,1,2]", 1882 ContentType: "application/json", 1883 }, 1884 }, 1885 } { 1886 t.Run(tc.path, func(subT *testing.T) { 1887 helper := test.New(subT) 1888 1889 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 1890 helper.Must(err) 1891 1892 res, err := client.Do(req) 1893 helper.Must(err) 1894 1895 resBytes, err := io.ReadAll(res.Body) 1896 helper.Must(err) 1897 res.Body.Close() 1898 1899 if string(resBytes) != tc.exp.Body { 1900 subT.Errorf("%s: want: %s, got:%s", tc.path, tc.exp.Body, string(resBytes)) 1901 } 1902 1903 if ct := res.Header.Get("Content-Type"); ct != tc.exp.ContentType { 1904 subT.Errorf("%s: want: %s, got:%s", tc.path, tc.exp.ContentType, ct) 1905 } 1906 }) 1907 } 1908 } 1909 1910 func TestHTTPServer_Endpoint_Evaluation(t *testing.T) { 1911 client := newClient() 1912 1913 confPath := path.Join("testdata/integration/endpoint_eval/01_couper.hcl") 1914 shutdown, _ := newCouper(confPath, test.New(t)) 1915 defer shutdown() 1916 1917 type expectation struct { 1918 Host, Origin, Path string 1919 } 1920 1921 type testCase struct { 1922 reqPath string 1923 exp expectation 1924 } 1925 1926 // first traffic pins the origin (transport conf) 1927 for _, tc := range []testCase{ 1928 {"/my-waffik/my.host.de/" + testBackend.Addr()[7:], expectation{ 1929 Host: "my.host.de", 1930 Origin: testBackend.Addr()[7:], 1931 Path: "/anything", 1932 }}, 1933 {"/my-respo/my.host.com/" + testBackend.Addr()[7:], expectation{ 1934 Host: "my.host.de", 1935 Origin: testBackend.Addr()[7:], 1936 Path: "/anything", 1937 }}, 1938 } { 1939 t.Run("_"+tc.reqPath, func(subT *testing.T) { 1940 helper := test.New(subT) 1941 1942 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.reqPath, nil) 1943 helper.Must(err) 1944 1945 res, err := client.Do(req) 1946 helper.Must(err) 1947 1948 resBytes, err := io.ReadAll(res.Body) 1949 helper.Must(err) 1950 1951 _ = res.Body.Close() 1952 1953 var jsonResult expectation 1954 err = json.Unmarshal(resBytes, &jsonResult) 1955 if err != nil { 1956 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 1957 } 1958 1959 jsonResult.Origin = res.Header.Get("X-Origin") 1960 1961 if !reflect.DeepEqual(jsonResult, tc.exp) { 1962 subT.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes)) 1963 } 1964 }) 1965 } 1966 } 1967 1968 func TestHTTPServer_Endpoint_Response_FormQuery_Evaluation(t *testing.T) { 1969 client := newClient() 1970 1971 confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl") 1972 shutdown, _ := newCouper(confPath, test.New(t)) 1973 defer shutdown() 1974 1975 helper := test.New(t) 1976 1977 req, err := http.NewRequest(http.MethodPost, "http://example.com:8080/req?foo=bar", strings.NewReader("s=abc123")) 1978 helper.Must(err) 1979 req.Header.Set("User-Agent", "") 1980 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 1981 1982 res, err := client.Do(req) 1983 helper.Must(err) 1984 1985 resBytes, err := io.ReadAll(res.Body) 1986 helper.Must(err) 1987 1988 _ = res.Body.Close() 1989 1990 type Expectation struct { 1991 FormBody url.Values `json:"form_body"` 1992 Headers test.Header `json:"headers"` 1993 Method string `json:"method"` 1994 Query url.Values `json:"query"` 1995 URL string `json:"url"` 1996 } 1997 1998 var jsonResult Expectation 1999 err = json.Unmarshal(resBytes, &jsonResult) 2000 if err != nil { 2001 t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 2002 } 2003 2004 delete(jsonResult.Headers, "couper-request-id") 2005 2006 exp := Expectation{ 2007 Method: http.MethodPost, 2008 FormBody: map[string][]string{ 2009 "s": {"abc123"}, 2010 }, 2011 Headers: map[string]string{ 2012 "content-length": "8", 2013 "content-type": "application/x-www-form-urlencoded", 2014 }, 2015 Query: map[string][]string{ 2016 "foo": {"bar"}, 2017 }, 2018 URL: "http://example.com:8080/req?foo=bar", 2019 } 2020 if !reflect.DeepEqual(jsonResult, exp) { 2021 t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes)) 2022 } 2023 } 2024 2025 func TestHTTPServer_Endpoint_Response_JSONBody_Evaluation(t *testing.T) { 2026 client := newClient() 2027 2028 confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl") 2029 shutdown, _ := newCouper(confPath, test.New(t)) 2030 defer shutdown() 2031 2032 helper := test.New(t) 2033 2034 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/req?foo=bar", strings.NewReader(`{"data": true}`)) 2035 helper.Must(err) 2036 req.Header.Set("User-Agent", "") 2037 req.Header.Set("Content-Type", "application/json") 2038 2039 res, err := client.Do(req) 2040 helper.Must(err) 2041 2042 resBytes, err := io.ReadAll(res.Body) 2043 helper.Must(err) 2044 2045 _ = res.Body.Close() 2046 2047 type Expectation struct { 2048 JSONBody map[string]interface{} `json:"json_body"` 2049 Headers test.Header `json:"headers"` 2050 Method string `json:"method"` 2051 Query url.Values `json:"query"` 2052 URL string `json:"url"` 2053 } 2054 2055 var jsonResult Expectation 2056 err = json.Unmarshal(resBytes, &jsonResult) 2057 if err != nil { 2058 t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 2059 } 2060 2061 delete(jsonResult.Headers, "couper-request-id") 2062 2063 exp := Expectation{ 2064 Method: http.MethodGet, 2065 JSONBody: map[string]interface{}{ 2066 "data": true, 2067 }, 2068 Headers: map[string]string{ 2069 "content-length": "14", 2070 "content-type": "application/json", 2071 }, 2072 Query: map[string][]string{ 2073 "foo": {"bar"}, 2074 }, 2075 URL: "http://example.com:8080/req?foo=bar", 2076 } 2077 if !reflect.DeepEqual(jsonResult, exp) { 2078 t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes)) 2079 } 2080 } 2081 2082 func TestHTTPServer_Endpoint_Response_JSONBody_Array_Evaluation(t *testing.T) { 2083 client := newClient() 2084 2085 confPath := path.Join("testdata/integration/endpoint_eval/15_couper.hcl") 2086 shutdown, _ := newCouper(confPath, test.New(t)) 2087 defer shutdown() 2088 2089 helper := test.New(t) 2090 2091 content := `[1, 2, {"data": true}]` 2092 2093 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/req?foo=bar", strings.NewReader(content)) 2094 helper.Must(err) 2095 req.Header.Set("User-Agent", "") 2096 req.Header.Set("Content-Type", "application/json") 2097 2098 res, err := client.Do(req) 2099 helper.Must(err) 2100 2101 resBytes, err := io.ReadAll(res.Body) 2102 helper.Must(err) 2103 2104 _ = res.Body.Close() 2105 2106 type Expectation struct { 2107 JSONBody interface{} `json:"json_body"` 2108 Headers test.Header `json:"headers"` 2109 Method string `json:"method"` 2110 Query url.Values `json:"query"` 2111 URL string `json:"url"` 2112 } 2113 2114 var jsonResult Expectation 2115 err = json.Unmarshal(resBytes, &jsonResult) 2116 if err != nil { 2117 t.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 2118 } 2119 2120 delete(jsonResult.Headers, "couper-request-id") 2121 2122 exp := Expectation{ 2123 Method: http.MethodGet, 2124 JSONBody: []interface{}{ 2125 1, 2126 2, 2127 map[string]interface{}{ 2128 "data": true, 2129 }, 2130 }, 2131 Headers: map[string]string{ 2132 "content-length": strconv.Itoa(len(content)), 2133 "content-type": "application/json", 2134 }, 2135 Query: map[string][]string{ 2136 "foo": {"bar"}, 2137 }, 2138 URL: "http://example.com:8080/req?foo=bar", 2139 } 2140 2141 if fmt.Sprint(jsonResult) != fmt.Sprint(exp) { 2142 t.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", exp, jsonResult, string(resBytes)) 2143 } 2144 } 2145 2146 func TestHTTPServer_AcceptingForwardedURL(t *testing.T) { 2147 client := newClient() 2148 2149 confPath := path.Join("testdata/settings/05_couper.hcl") 2150 shutdown, hook := newCouper(confPath, test.New(t)) 2151 defer shutdown() 2152 2153 type expectation struct { 2154 Protocol string `json:"protocol"` 2155 Host string `json:"host"` 2156 Port int `json:"port"` 2157 Origin string `json:"origin"` 2158 URL string `json:"url"` 2159 } 2160 2161 type testCase struct { 2162 name string 2163 header http.Header 2164 exp expectation 2165 wantAccessLogURL string 2166 } 2167 2168 for _, tc := range []testCase{ 2169 { 2170 "no proto, host, or port", 2171 http.Header{}, 2172 expectation{ 2173 Protocol: "http", 2174 Host: "localhost", 2175 Port: 8080, 2176 Origin: "http://localhost:8080", 2177 URL: "http://localhost:8080/path", 2178 }, 2179 "http://localhost:8080/path", 2180 }, 2181 { 2182 "port, no proto, no host", 2183 http.Header{ 2184 "X-Forwarded-Port": []string{"8081"}, 2185 }, 2186 expectation{ 2187 Protocol: "http", 2188 Host: "localhost", 2189 Port: 8081, 2190 Origin: "http://localhost:8081", 2191 URL: "http://localhost:8081/path", 2192 }, 2193 "http://localhost:8081/path", 2194 }, 2195 { 2196 "proto, no host, no port", 2197 http.Header{ 2198 "X-Forwarded-Proto": []string{"https"}, 2199 }, 2200 expectation{ 2201 Protocol: "https", 2202 Host: "localhost", 2203 Port: 443, 2204 Origin: "https://localhost", 2205 URL: "https://localhost/path", 2206 }, 2207 "https://localhost/path", 2208 }, 2209 { 2210 "proto, host, no port", 2211 http.Header{ 2212 "X-Forwarded-Proto": []string{"https"}, 2213 "X-Forwarded-Host": []string{"www.example.com"}, 2214 }, 2215 expectation{ 2216 Protocol: "https", 2217 Host: "www.example.com", 2218 Port: 443, 2219 Origin: "https://www.example.com", 2220 URL: "https://www.example.com/path", 2221 }, 2222 "https://www.example.com/path", 2223 }, 2224 { 2225 "proto, host with port, no port", 2226 http.Header{ 2227 "X-Forwarded-Proto": []string{"https"}, 2228 "X-Forwarded-Host": []string{"www.example.com:8443"}, 2229 }, 2230 expectation{ 2231 Protocol: "https", 2232 Host: "www.example.com", 2233 Port: 8443, 2234 Origin: "https://www.example.com:8443", 2235 URL: "https://www.example.com:8443/path", 2236 }, 2237 "https://www.example.com:8443/path", 2238 }, 2239 { 2240 "proto, port, no host", 2241 http.Header{ 2242 "X-Forwarded-Proto": []string{"https"}, 2243 "X-Forwarded-Port": []string{"8443"}, 2244 }, 2245 expectation{ 2246 Protocol: "https", 2247 Host: "localhost", 2248 Port: 8443, 2249 Origin: "https://localhost:8443", 2250 URL: "https://localhost:8443/path", 2251 }, 2252 "https://localhost:8443/path", 2253 }, 2254 { 2255 "host, port, no proto", 2256 http.Header{ 2257 "X-Forwarded-Host": []string{"www.example.com"}, 2258 "X-Forwarded-Port": []string{"8081"}, 2259 }, 2260 expectation{ 2261 Protocol: "http", 2262 Host: "www.example.com", 2263 Port: 8081, 2264 Origin: "http://www.example.com:8081", 2265 URL: "http://www.example.com:8081/path", 2266 }, 2267 "http://www.example.com:8081/path", 2268 }, 2269 { 2270 "host with port, port, no proto", 2271 http.Header{ 2272 "X-Forwarded-Host": []string{"www.example.com:8081"}, 2273 "X-Forwarded-Port": []string{"8081"}, 2274 }, 2275 expectation{ 2276 Protocol: "http", 2277 Host: "www.example.com", 2278 Port: 8081, 2279 Origin: "http://www.example.com:8081", 2280 URL: "http://www.example.com:8081/path", 2281 }, 2282 "http://www.example.com:8081/path", 2283 }, 2284 { 2285 "host with port, different port, no proto", 2286 http.Header{ 2287 "X-Forwarded-Host": []string{"www.example.com:8081"}, 2288 "X-Forwarded-Port": []string{"8082"}, 2289 }, 2290 expectation{ 2291 Protocol: "http", 2292 Host: "www.example.com", 2293 Port: 8082, 2294 Origin: "http://www.example.com:8082", 2295 URL: "http://www.example.com:8082/path", 2296 }, 2297 "http://www.example.com:8082/path", 2298 }, 2299 { 2300 "host, no port, no proto", 2301 http.Header{ 2302 "X-Forwarded-Host": []string{"www.example.com"}, 2303 }, 2304 expectation{ 2305 Protocol: "http", 2306 Host: "www.example.com", 2307 Port: 8080, 2308 Origin: "http://www.example.com:8080", 2309 URL: "http://www.example.com:8080/path", 2310 }, 2311 "http://www.example.com:8080/path", 2312 }, 2313 { 2314 "host with port, no proto, no port", 2315 http.Header{ 2316 "X-Forwarded-Host": []string{"www.example.com:8081"}, 2317 }, 2318 expectation{ 2319 Protocol: "http", 2320 Host: "www.example.com", 2321 Port: 8081, 2322 Origin: "http://www.example.com:8081", 2323 URL: "http://www.example.com:8081/path", 2324 }, 2325 "http://www.example.com:8081/path", 2326 }, 2327 { 2328 "proto, host, port", 2329 http.Header{ 2330 "X-Forwarded-Proto": []string{"https"}, 2331 "X-Forwarded-Host": []string{"www.example.com"}, 2332 "X-Forwarded-Port": []string{"8443"}, 2333 }, 2334 expectation{ 2335 Protocol: "https", 2336 Host: "www.example.com", 2337 Port: 8443, 2338 Origin: "https://www.example.com:8443", 2339 URL: "https://www.example.com:8443/path", 2340 }, 2341 "https://www.example.com:8443/path", 2342 }, 2343 { 2344 "proto, host with port, port", 2345 http.Header{ 2346 "X-Forwarded-Proto": []string{"https"}, 2347 "X-Forwarded-Host": []string{"www.example.com:8443"}, 2348 "X-Forwarded-Port": []string{"8443"}, 2349 }, 2350 expectation{ 2351 Protocol: "https", 2352 Host: "www.example.com", 2353 Port: 8443, 2354 Origin: "https://www.example.com:8443", 2355 URL: "https://www.example.com:8443/path", 2356 }, 2357 "https://www.example.com:8443/path", 2358 }, 2359 { 2360 "proto, host with port, different port", 2361 http.Header{ 2362 "X-Forwarded-Proto": []string{"https"}, 2363 "X-Forwarded-Host": []string{"www.example.com:8443"}, 2364 "X-Forwarded-Port": []string{"9443"}, 2365 }, 2366 expectation{ 2367 Protocol: "https", 2368 Host: "www.example.com", 2369 Port: 9443, 2370 Origin: "https://www.example.com:9443", 2371 URL: "https://www.example.com:9443/path", 2372 }, 2373 "https://www.example.com:9443/path", 2374 }, 2375 } { 2376 t.Run(tc.name, func(subT *testing.T) { 2377 helper := test.New(subT) 2378 hook.Reset() 2379 2380 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/path", nil) 2381 helper.Must(err) 2382 for k, v := range tc.header { 2383 req.Header.Set(k, v[0]) 2384 } 2385 2386 res, err := client.Do(req) 2387 helper.Must(err) 2388 2389 resBytes, err := io.ReadAll(res.Body) 2390 helper.Must(err) 2391 2392 _ = res.Body.Close() 2393 2394 var jsonResult expectation 2395 err = json.Unmarshal(resBytes, &jsonResult) 2396 if err != nil { 2397 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 2398 } 2399 if !reflect.DeepEqual(jsonResult, tc.exp) { 2400 subT.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.exp, jsonResult, string(resBytes)) 2401 } 2402 2403 logURL := getAccessLogURL(hook) 2404 if logURL != tc.wantAccessLogURL { 2405 subT.Errorf("Expected URL: %q, actual: %q", tc.wantAccessLogURL, logURL) 2406 } 2407 }) 2408 } 2409 } 2410 2411 func TestHTTPServer_XFH_AcceptingForwardedURL(t *testing.T) { 2412 client := newClient() 2413 2414 confPath := path.Join("testdata/settings/06_couper.hcl") 2415 shutdown, hook := newCouper(confPath, test.New(t)) 2416 defer shutdown() 2417 2418 type expectation struct { 2419 Protocol string `json:"protocol"` 2420 Host string `json:"host"` 2421 Port int `json:"port"` 2422 Origin string `json:"origin"` 2423 URL string `json:"url"` 2424 } 2425 2426 type testCase struct { 2427 name string 2428 header http.Header 2429 exp expectation 2430 wantAccessLogURL string 2431 } 2432 2433 for _, tc := range []testCase{ 2434 { 2435 "no proto, host, or port", 2436 http.Header{}, 2437 expectation{ 2438 Protocol: "http", 2439 Host: "localhost", 2440 Port: 8080, 2441 Origin: "http://localhost:8080", 2442 URL: "http://localhost:8080/path", 2443 }, 2444 "http://localhost:8080/path", 2445 }, 2446 { 2447 "port, no proto, no host", 2448 http.Header{ 2449 "X-Forwarded-Port": []string{"8081"}, 2450 }, 2451 expectation{ 2452 Protocol: "http", 2453 Host: "localhost", 2454 Port: 8081, 2455 Origin: "http://localhost:8081", 2456 URL: "http://localhost:8081/path", 2457 }, 2458 "http://localhost:8081/path", 2459 }, 2460 { 2461 "proto, no host, no port", 2462 http.Header{ 2463 "X-Forwarded-Proto": []string{"https"}, 2464 }, 2465 expectation{ 2466 Protocol: "https", 2467 Host: "localhost", 2468 Port: 443, 2469 Origin: "https://localhost", 2470 URL: "https://localhost/path", 2471 }, 2472 "https://localhost/path", 2473 }, 2474 { 2475 "proto, host, no port", 2476 http.Header{ 2477 "X-Forwarded-Proto": []string{"https"}, 2478 "X-Forwarded-Host": []string{"www.example.com"}, 2479 }, 2480 expectation{ 2481 Protocol: "https", 2482 Host: "www.example.com", 2483 Port: 443, 2484 Origin: "https://www.example.com", 2485 URL: "https://www.example.com/path", 2486 }, 2487 "https://www.example.com/path", 2488 }, 2489 { 2490 "proto, host with port, no port", 2491 http.Header{ 2492 "X-Forwarded-Proto": []string{"https"}, 2493 "X-Forwarded-Host": []string{"www.example.com:8443"}, 2494 }, 2495 expectation{ 2496 Protocol: "https", 2497 Host: "www.example.com", 2498 Port: 443, 2499 Origin: "https://www.example.com", 2500 URL: "https://www.example.com/path", 2501 }, 2502 "https://www.example.com/path", 2503 }, 2504 { 2505 "proto, port, no host", 2506 http.Header{ 2507 "X-Forwarded-Proto": []string{"https"}, 2508 "X-Forwarded-Port": []string{"8443"}, 2509 }, 2510 expectation{ 2511 Protocol: "https", 2512 Host: "localhost", 2513 Port: 8443, 2514 Origin: "https://localhost:8443", 2515 URL: "https://localhost:8443/path", 2516 }, 2517 "https://localhost:8443/path", 2518 }, 2519 { 2520 "host, port, no proto", 2521 http.Header{ 2522 "X-Forwarded-Host": []string{"www.example.com"}, 2523 "X-Forwarded-Port": []string{"8081"}, 2524 }, 2525 expectation{ 2526 Protocol: "http", 2527 Host: "www.example.com", 2528 Port: 8081, 2529 Origin: "http://www.example.com:8081", 2530 URL: "http://www.example.com:8081/path", 2531 }, 2532 "http://www.example.com:8081/path", 2533 }, 2534 { 2535 "host with port, port, no proto", 2536 http.Header{ 2537 "X-Forwarded-Host": []string{"www.example.com:8081"}, 2538 "X-Forwarded-Port": []string{"8081"}, 2539 }, 2540 expectation{ 2541 Protocol: "http", 2542 Host: "www.example.com", 2543 Port: 8081, 2544 Origin: "http://www.example.com:8081", 2545 URL: "http://www.example.com:8081/path", 2546 }, 2547 "http://www.example.com:8081/path", 2548 }, 2549 { 2550 "host with port, different port, no proto", 2551 http.Header{ 2552 "X-Forwarded-Host": []string{"www.example.com:8081"}, 2553 "X-Forwarded-Port": []string{"8082"}, 2554 }, 2555 expectation{ 2556 Protocol: "http", 2557 Host: "www.example.com", 2558 Port: 8082, 2559 Origin: "http://www.example.com:8082", 2560 URL: "http://www.example.com:8082/path", 2561 }, 2562 "http://www.example.com:8082/path", 2563 }, 2564 { 2565 "host, no port, no proto", 2566 http.Header{ 2567 "X-Forwarded-Host": []string{"www.example.com"}, 2568 }, 2569 expectation{ 2570 Protocol: "http", 2571 Host: "www.example.com", 2572 Port: 8080, 2573 Origin: "http://www.example.com:8080", 2574 URL: "http://www.example.com:8080/path", 2575 }, 2576 "http://www.example.com:8080/path", 2577 }, 2578 { 2579 "host with port, no proto, no port", 2580 http.Header{ 2581 "X-Forwarded-Host": []string{"www.example.com:8081"}, 2582 }, 2583 expectation{ 2584 Protocol: "http", 2585 Host: "www.example.com", 2586 Port: 8080, 2587 Origin: "http://www.example.com:8080", 2588 URL: "http://www.example.com:8080/path", 2589 }, 2590 "http://www.example.com:8080/path", 2591 }, 2592 { 2593 "proto, host, port", 2594 http.Header{ 2595 "X-Forwarded-Proto": []string{"https"}, 2596 "X-Forwarded-Host": []string{"www.example.com"}, 2597 "X-Forwarded-Port": []string{"8443"}, 2598 }, 2599 expectation{ 2600 Protocol: "https", 2601 Host: "www.example.com", 2602 Port: 8443, 2603 Origin: "https://www.example.com:8443", 2604 URL: "https://www.example.com:8443/path", 2605 }, 2606 "https://www.example.com:8443/path", 2607 }, 2608 { 2609 "proto, host with port, port", 2610 http.Header{ 2611 "X-Forwarded-Proto": []string{"https"}, 2612 "X-Forwarded-Host": []string{"www.example.com:8443"}, 2613 "X-Forwarded-Port": []string{"8443"}, 2614 }, 2615 expectation{ 2616 Protocol: "https", 2617 Host: "www.example.com", 2618 Port: 8443, 2619 Origin: "https://www.example.com:8443", 2620 URL: "https://www.example.com:8443/path", 2621 }, 2622 "https://www.example.com:8443/path", 2623 }, 2624 { 2625 "proto, host with port, different port", 2626 http.Header{ 2627 "X-Forwarded-Proto": []string{"https"}, 2628 "X-Forwarded-Host": []string{"www.example.com:8443"}, 2629 "X-Forwarded-Port": []string{"9443"}, 2630 }, 2631 expectation{ 2632 Protocol: "https", 2633 Host: "www.example.com", 2634 Port: 9443, 2635 Origin: "https://www.example.com:9443", 2636 URL: "https://www.example.com:9443/path", 2637 }, 2638 "https://www.example.com:9443/path", 2639 }, 2640 } { 2641 t.Run(tc.name, func(subT *testing.T) { 2642 helper := test.New(subT) 2643 hook.Reset() 2644 2645 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/path", nil) 2646 helper.Must(err) 2647 for k, v := range tc.header { 2648 req.Header.Set(k, v[0]) 2649 } 2650 2651 res, err := client.Do(req) 2652 helper.Must(err) 2653 2654 resBytes, err := io.ReadAll(res.Body) 2655 helper.Must(err) 2656 2657 _ = res.Body.Close() 2658 2659 var jsonResult expectation 2660 err = json.Unmarshal(resBytes, &jsonResult) 2661 if err != nil { 2662 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 2663 } 2664 if !reflect.DeepEqual(jsonResult, tc.exp) { 2665 subT.Errorf("\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.exp, jsonResult, string(resBytes)) 2666 } 2667 2668 logURL := getAccessLogURL(hook) 2669 if logURL != tc.wantAccessLogURL { 2670 subT.Errorf("Expected URL: %q, actual: %q", tc.wantAccessLogURL, logURL) 2671 } 2672 }) 2673 } 2674 } 2675 2676 func TestHTTPServer_BackendProbes(t *testing.T) { 2677 helper := test.New(t) 2678 client := newClient() 2679 2680 confPath := path.Join("testdata/integration/config/14_couper.hcl") 2681 shutdown, _ := newCouper(confPath, helper) 2682 defer shutdown() 2683 2684 type testCase struct { 2685 name string 2686 path string 2687 expect string 2688 } 2689 2690 time.Sleep(2 * time.Second) 2691 healthyJSON := `{"error":"","healthy":true,"state":"healthy"}` 2692 2693 for _, tc := range []testCase{ 2694 { 2695 "unknown backend", 2696 "/unknown", 2697 `null`, 2698 }, 2699 { 2700 "healthy backend", 2701 "/healthy/default", 2702 healthyJSON, 2703 }, 2704 { 2705 "healthy backend w/ expected_status", 2706 "/healthy/expected_status", 2707 healthyJSON, 2708 }, 2709 { 2710 "healthy backend w/ expected_text", 2711 "/healthy/expected_text", 2712 healthyJSON, 2713 }, 2714 { 2715 "healthy backend w/ path", 2716 "/healthy/path", 2717 healthyJSON, 2718 }, 2719 { 2720 "healthy backend w/ headers", 2721 "/healthy/headers", 2722 healthyJSON, 2723 }, 2724 { 2725 "healthy backend w/ fallback ua header", 2726 "/healthy/ua-header", 2727 healthyJSON, 2728 }, 2729 { 2730 "healthy backend: check does not follow Location", 2731 "/healthy/no_follow_redirect", 2732 healthyJSON, 2733 }, 2734 { 2735 "unhealthy backend: timeout", 2736 "/unhealthy/timeout", 2737 `{"error":"backend error: connecting to unhealthy_timeout '1.2.3.4' failed: i/o timeout","healthy":false,"state":"unhealthy"}`, 2738 }, 2739 { 2740 "unhealthy backend: unexpected status code", 2741 "/unhealthy/bad_status", 2742 `{"error":"unexpected status code: 404","healthy":false,"state":"unhealthy"}`, 2743 }, 2744 { 2745 "unhealthy backend w/ expected_status: unexpected status code", 2746 "/unhealthy/bad_expected_status", 2747 `{"error":"unexpected status code: 200","healthy":false,"state":"unhealthy"}`, 2748 }, 2749 { 2750 "unhealthy backend w/ expected_text: unexpected text", 2751 "/unhealthy/bad_expected_text", 2752 `{"error":"unexpected text","healthy":false,"state":"unhealthy"}`, 2753 }, 2754 { 2755 "unhealthy backend: unexpected status code", 2756 "/unhealthy/bad_status", 2757 `{"error":"unexpected status code: 404","healthy":false,"state":"unhealthy"}`, 2758 }, 2759 { 2760 "unhealthy backend w/ path: unexpected status code", 2761 "/unhealthy/bad_path", 2762 `{"error":"unexpected status code: 404","healthy":false,"state":"unhealthy"}`, 2763 }, 2764 { 2765 "unhealthy backend w/ headers: unexpected text", 2766 "/unhealthy/headers", 2767 `{"error":"unexpected text","healthy":false,"state":"unhealthy"}`, 2768 }, 2769 { 2770 "unhealthy backend: does not follow location", 2771 "/unhealthy/no_follow_redirect", 2772 `{"error":"unexpected status code: 302","healthy":false,"state":"unhealthy"}`, 2773 }, 2774 { 2775 "backend error: timeout but threshold not reached", 2776 "/failing", 2777 `{"error":"backend error: connecting to failing '1.2.3.4' failed: i/o timeout","healthy":true,"state":"failing"}`, 2778 }, 2779 } { 2780 t.Run(tc.name, func(subT *testing.T) { 2781 h := test.New(subT) 2782 2783 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 2784 h.Must(err) 2785 2786 res, err := client.Do(req) 2787 h.Must(err) 2788 2789 b, _ := io.ReadAll(res.Body) 2790 body := string(b) 2791 h.Must(res.Body.Close()) 2792 2793 if body != tc.expect { 2794 t.Errorf("%s: Unexpected states:\n\tWant: %s\n\tGot: %s", tc.name, tc.expect, body) 2795 } 2796 }) 2797 } 2798 } 2799 2800 func TestHTTPServer_backend_requests_variables(t *testing.T) { 2801 client := newClient() 2802 2803 ResourceOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 2804 rw.WriteHeader(http.StatusNoContent) 2805 })) 2806 defer ResourceOrigin.Close() 2807 2808 confPath := path.Join("testdata/integration/endpoint_eval/18_couper.hcl") 2809 shutdown, hook, err := newCouperWithTemplate(confPath, test.New(t), map[string]interface{}{"rsOrigin": ResourceOrigin.URL}) 2810 if err != nil { 2811 t.Fatal(err) 2812 } 2813 defer shutdown() 2814 2815 type expectation struct { 2816 Method string `json:"method"` 2817 Protocol string `json:"protocol"` 2818 Host string `json:"host"` 2819 Port int64 `json:"port"` 2820 Path string `json:"path"` 2821 Query map[string][]string `json:"query"` 2822 Origin string `json:"origin"` 2823 URL string `json:"url"` 2824 Body string `json:"body"` 2825 JSONBody map[string]interface{} `json:"json_body"` 2826 FormBody map[string][]string `json:"form_body"` 2827 } 2828 2829 type testCase struct { 2830 name string 2831 relURL string 2832 header http.Header 2833 body io.Reader 2834 exp expectation 2835 } 2836 2837 helper := test.New(t) 2838 resourceOrigin, perr := url.Parse(ResourceOrigin.URL) 2839 helper.Must(perr) 2840 2841 port, _ := strconv.ParseInt(resourceOrigin.Port(), 10, 64) 2842 2843 for _, tc := range []testCase{ 2844 { 2845 "body", 2846 "/body", 2847 http.Header{}, 2848 strings.NewReader(`abcd1234`), 2849 expectation{ 2850 Method: http.MethodPost, 2851 Protocol: resourceOrigin.Scheme, 2852 Host: resourceOrigin.Hostname(), 2853 Port: port, 2854 Path: "/resource", 2855 Query: map[string][]string{"foo": {"bar"}}, 2856 Origin: ResourceOrigin.URL, 2857 URL: ResourceOrigin.URL + "/resource?foo=bar", 2858 Body: "abcd1234", 2859 JSONBody: map[string]interface{}{}, 2860 FormBody: map[string][]string{}, 2861 }, 2862 }, 2863 { 2864 "json_body", 2865 "/json_body", 2866 http.Header{"Content-Type": []string{"application/json"}}, 2867 strings.NewReader(`{"s":"abcd1234"}`), 2868 expectation{ 2869 Method: http.MethodPost, 2870 Protocol: resourceOrigin.Scheme, 2871 Host: resourceOrigin.Hostname(), 2872 Port: port, 2873 Path: "/resource", 2874 Query: map[string][]string{"foo": {"bar"}}, 2875 Origin: ResourceOrigin.URL, 2876 URL: ResourceOrigin.URL + "/resource?foo=bar", 2877 Body: `{"s":"abcd1234"}`, 2878 JSONBody: map[string]interface{}{"s": "abcd1234"}, 2879 FormBody: map[string][]string{}, 2880 }, 2881 }, 2882 { 2883 "form_body", 2884 "/form_body", 2885 http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}}, 2886 strings.NewReader(`s=abcd1234`), 2887 expectation{ 2888 Method: http.MethodPost, 2889 Protocol: resourceOrigin.Scheme, 2890 Host: resourceOrigin.Hostname(), 2891 Port: port, 2892 Path: "/resource", 2893 Query: map[string][]string{"foo": {"bar"}}, 2894 Origin: ResourceOrigin.URL, 2895 URL: ResourceOrigin.URL + "/resource?foo=bar", 2896 Body: `s=abcd1234`, 2897 JSONBody: map[string]interface{}{}, 2898 FormBody: map[string][]string{"s": {"abcd1234"}}, 2899 }, 2900 }, 2901 } { 2902 t.Run(tc.name, func(subT *testing.T) { 2903 h := test.New(subT) 2904 hook.Reset() 2905 2906 req, err := http.NewRequest(http.MethodPost, "http://localhost:8080"+tc.relURL, tc.body) 2907 h.Must(err) 2908 2909 for k, v := range tc.header { 2910 req.Header.Set(k, v[0]) 2911 } 2912 2913 res, err := client.Do(req) 2914 h.Must(err) 2915 2916 resBytes, err := io.ReadAll(res.Body) 2917 h.Must(err) 2918 2919 _ = res.Body.Close() 2920 2921 var jsonResult expectation 2922 err = json.Unmarshal(resBytes, &jsonResult) 2923 if err != nil { 2924 subT.Errorf("%s: unmarshal json: %v: got:\n%s", tc.name, err, string(resBytes)) 2925 } 2926 if !reflect.DeepEqual(jsonResult, tc.exp) { 2927 subT.Errorf("%s\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.name, tc.exp, jsonResult, string(resBytes)) 2928 } 2929 }) 2930 } 2931 } 2932 2933 func TestHTTPServer_request_variables(t *testing.T) { 2934 client := newClient() 2935 2936 confPath := path.Join("testdata/integration/endpoint_eval/19_couper.hcl") 2937 shutdown, hook := newCouper(confPath, test.New(t)) 2938 defer shutdown() 2939 2940 type expectation struct { 2941 Method string `json:"method"` 2942 Protocol string `json:"protocol"` 2943 Host string `json:"host"` 2944 Port int64 `json:"port"` 2945 Path string `json:"path"` 2946 Query map[string][]string `json:"query"` 2947 Origin string `json:"origin"` 2948 URL string `json:"url"` 2949 Body string `json:"body"` 2950 JSONBody map[string]interface{} `json:"json_body"` 2951 FormBody map[string][]string `json:"form_body"` 2952 } 2953 2954 type testCase struct { 2955 name string 2956 relURL string 2957 header http.Header 2958 body io.Reader 2959 exp expectation 2960 } 2961 2962 for _, tc := range []testCase{ 2963 { 2964 "body", 2965 "/body?foo=bar", 2966 http.Header{}, 2967 strings.NewReader(`abcd1234`), 2968 expectation{ 2969 Method: "POST", 2970 Protocol: "http", 2971 Host: "localhost", 2972 Port: 8080, 2973 Path: "/body", 2974 Query: map[string][]string{"foo": {"bar"}}, 2975 Origin: "http://localhost:8080", 2976 URL: "http://localhost:8080/body?foo=bar", 2977 Body: "abcd1234", 2978 JSONBody: map[string]interface{}{}, 2979 FormBody: map[string][]string{}, 2980 }, 2981 }, 2982 { 2983 "json_body", 2984 "/json_body?foo=bar", 2985 http.Header{"Content-Type": []string{"application/json"}}, 2986 strings.NewReader(`{"s":"abcd1234"}`), 2987 expectation{ 2988 Method: "POST", 2989 Protocol: "http", 2990 Host: "localhost", 2991 Port: 8080, 2992 Path: "/json_body", 2993 Query: map[string][]string{"foo": {"bar"}}, 2994 Origin: "http://localhost:8080", 2995 URL: "http://localhost:8080/json_body?foo=bar", 2996 Body: `{"s":"abcd1234"}`, 2997 JSONBody: map[string]interface{}{"s": "abcd1234"}, 2998 FormBody: map[string][]string{}, 2999 }, 3000 }, 3001 { 3002 "form_body", 3003 "/form_body?foo=bar", 3004 http.Header{"Content-Type": []string{"application/x-www-form-urlencoded"}}, 3005 strings.NewReader(`s=abcd1234`), 3006 expectation{ 3007 Method: "POST", 3008 Protocol: "http", 3009 Host: "localhost", 3010 Port: 8080, 3011 Path: "/form_body", 3012 Query: map[string][]string{"foo": {"bar"}}, 3013 Origin: "http://localhost:8080", 3014 URL: "http://localhost:8080/form_body?foo=bar", 3015 Body: `s=abcd1234`, 3016 JSONBody: map[string]interface{}{}, 3017 FormBody: map[string][]string{"s": {"abcd1234"}}, 3018 }, 3019 }, 3020 } { 3021 t.Run(tc.name, func(subT *testing.T) { 3022 helper := test.New(subT) 3023 hook.Reset() 3024 3025 req, err := http.NewRequest(http.MethodPost, "http://localhost:8080"+tc.relURL, tc.body) 3026 helper.Must(err) 3027 3028 for k, v := range tc.header { 3029 req.Header.Set(k, v[0]) 3030 } 3031 3032 res, err := client.Do(req) 3033 helper.Must(err) 3034 3035 resBytes, err := io.ReadAll(res.Body) 3036 helper.Must(err) 3037 3038 _ = res.Body.Close() 3039 3040 var jsonResult expectation 3041 err = json.Unmarshal(resBytes, &jsonResult) 3042 if err != nil { 3043 subT.Errorf("%s: unmarshal json: %v: got:\n%s", tc.name, err, string(resBytes)) 3044 } 3045 if !reflect.DeepEqual(jsonResult, tc.exp) { 3046 subT.Errorf("%s\nwant:\t%#v\ngot:\t%#v\npayload: %s", tc.name, tc.exp, jsonResult, string(resBytes)) 3047 } 3048 }) 3049 } 3050 } 3051 3052 func TestOpenAPIValidateConcurrentRequests(t *testing.T) { 3053 helper := test.New(t) 3054 client := newClient() 3055 3056 shutdown, _ := newCouper("testdata/integration/validation/01_couper.hcl", helper) 3057 defer shutdown() 3058 3059 req1, err := http.NewRequest(http.MethodGet, "http://example.com:8080/anything", nil) 3060 helper.Must(err) 3061 req2, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pdf", nil) 3062 helper.Must(err) 3063 3064 var res1, res2 *http.Response 3065 var err1, err2 error 3066 waitCh := make(chan struct{}) 3067 wg := sync.WaitGroup{} 3068 wg.Add(2) 3069 go func() { 3070 defer wg.Done() 3071 <-waitCh // blocks 3072 res1, err1 = client.Do(req1) 3073 }() 3074 go func() { 3075 defer wg.Done() 3076 <-waitCh // blocks 3077 res2, err2 = client.Do(req2) 3078 }() 3079 3080 close(waitCh) // triggers reqs 3081 wg.Wait() 3082 3083 helper.Must(err1) 3084 helper.Must(err2) 3085 3086 if res1.StatusCode != 200 { 3087 t.Errorf("Expected status %d for response1; got: %d", 200, res1.StatusCode) 3088 } 3089 if res2.StatusCode != 502 { 3090 t.Errorf("Expected status %d for response2; got: %d", 502, res2.StatusCode) 3091 } 3092 } 3093 3094 func TestOpenAPIValidateRequestResponseBuffer(t *testing.T) { 3095 helper := test.New(t) 3096 3097 content := `{ "prop": true }` 3098 origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 3099 b, err := io.ReadAll(req.Body) 3100 helper.Must(err) 3101 if string(b) != content { 3102 t.Errorf("origin: expected same content") 3103 } 3104 rw.Header().Set("Content-Type", "application/json") 3105 _, err = rw.Write([]byte(content)) 3106 helper.Must(err) 3107 })) 3108 defer origin.Close() 3109 3110 shutdown, _ := newCouper("testdata/integration/validation/02_couper.hcl", helper) 3111 defer shutdown() 3112 3113 req, err := http.NewRequest(http.MethodPost, "http://localhost:8080/buffer", bytes.NewBufferString(content)) 3114 helper.Must(err) 3115 3116 req.Header.Set("Content-Type", "application/json") 3117 req.Header.Set("Origin", origin.URL) 3118 3119 res, err := test.NewHTTPClient().Do(req) 3120 helper.Must(err) 3121 3122 if res.StatusCode != http.StatusOK { 3123 t.Errorf("Expected StatusOK, got: %s", res.Status) 3124 } 3125 3126 b, err := io.ReadAll(res.Body) 3127 helper.Must(err) 3128 3129 helper.Must(res.Body.Close()) 3130 3131 if string(b) != content { 3132 t.Error("expected same body content") 3133 } 3134 } 3135 3136 func TestConfigBodyContent(t *testing.T) { 3137 helper := test.New(t) 3138 client := newClient() 3139 3140 expiredOrigin, selfSigned := test.NewExpiredBackend() 3141 defer expiredOrigin.Close() 3142 3143 expiredCert, err := os.CreateTemp(os.TempDir(), "expired.pem") 3144 helper.Must(err) 3145 3146 _, err = expiredCert.Write(selfSigned.CACertificate.Certificate) 3147 helper.Must(err) 3148 helper.Must(expiredCert.Close()) 3149 3150 defer os.RemoveAll(expiredCert.Name()) 3151 3152 shutdown, _, err := newCouperWithTemplate("testdata/integration/config/01_couper.hcl", helper, map[string]interface{}{ 3153 "expiredOrigin": expiredOrigin.Addr(), 3154 "caFile": expiredCert.Name(), 3155 }) 3156 helper.Must(err) 3157 defer shutdown() 3158 3159 // default port changed in config 3160 req, err := http.NewRequest(http.MethodGet, "http://time.out:8090/", nil) 3161 helper.Must(err) 3162 3163 // 2s timeout in config 3164 ctx, cancel := context.WithDeadline(req.Context(), time.Now().Add(time.Second*10)) 3165 defer cancel() 3166 *req = *req.Clone(ctx) 3167 defer func() { 3168 if e := ctx.Err(); e != nil { 3169 t.Error("Expected used config timeout instead of deadline timer") 3170 } 3171 }() 3172 3173 _, err = client.Do(req) 3174 helper.Must(err) 3175 3176 // disabled cert check in config 3177 req, err = http.NewRequest(http.MethodGet, "http://time.out:8090/expired/", nil) 3178 helper.Must(err) 3179 3180 res, err := client.Do(req) 3181 helper.Must(err) 3182 if res.StatusCode != http.StatusOK { 3183 t.Errorf("Expected status OK with disabled certificate validation, got: %q", res.Status) 3184 } 3185 } 3186 3187 func TestConfigBodyContentBackends(t *testing.T) { 3188 client := newClient() 3189 3190 shutdown, _ := newCouper("testdata/integration/config/02_couper.hcl", test.New(t)) 3191 defer shutdown() 3192 3193 type testCase struct { 3194 path string 3195 header http.Header 3196 query url.Values 3197 } 3198 3199 for _, tc := range []testCase{ 3200 {"/anything", http.Header{"Foo": []string{"4"}}, url.Values{"bar": []string{"3", "4"}}}, 3201 {"/get", http.Header{"Foo": []string{"1", "3"}}, url.Values{"bar": []string{"1", "4"}}}, 3202 } { 3203 t.Run(tc.path[1:], func(subT *testing.T) { 3204 helper := test.New(subT) 3205 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil) 3206 helper.Must(err) 3207 3208 res, err := client.Do(req) 3209 helper.Must(err) 3210 3211 if res.StatusCode != http.StatusOK { 3212 subT.Errorf("%q: expected Status OK, got: %d", tc.path, res.StatusCode) 3213 } 3214 3215 b, err := io.ReadAll(res.Body) 3216 helper.Must(err) 3217 3218 type payload struct { 3219 Query url.Values 3220 } 3221 var p payload 3222 helper.Must(json.Unmarshal(b, &p)) 3223 3224 for k, v := range tc.header { 3225 if !reflect.DeepEqual(res.Header[k], v) { 3226 subT.Errorf("Expected Header %q value: %v, got: %v", k, v, res.Header[k]) 3227 } 3228 } 3229 3230 for k, v := range tc.query { 3231 if !reflect.DeepEqual(p.Query[k], v) { 3232 subT.Errorf("Expected Query %q value: %v, got: %v", k, v, p.Query[k]) 3233 } 3234 } 3235 }) 3236 } 3237 } 3238 3239 func TestConfigBodyContentAccessControl(t *testing.T) { 3240 client := newClient() 3241 3242 shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t)) 3243 defer shutdown() 3244 3245 type testCase struct { 3246 path string 3247 header http.Header 3248 status int 3249 ct string 3250 wantErrLog string 3251 } 3252 3253 for _, tc := range []testCase{ 3254 {"/v1", http.Header{"Auth": []string{"ba1"}}, http.StatusOK, "application/json", ""}, 3255 {"/v2", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba2"}}, http.StatusOK, "application/json", ""}, // minimum ':' 3256 {"/v2", http.Header{}, http.StatusUnauthorized, "application/json", "access control error: ba1: credentials required"}, 3257 {"/v3", http.Header{}, http.StatusOK, "application/json", ""}, 3258 {"/status", http.Header{}, http.StatusOK, "application/json", ""}, 3259 {"/superadmin", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba4"}}, http.StatusOK, "application/json", ""}, 3260 {"/superadmin", http.Header{}, http.StatusUnauthorized, "application/json", "access control error: ba1: credentials required"}, 3261 {"/ba5", http.Header{"Authorization": []string{"Basic VVNSOlBXRA=="}, "X-Ba-User": []string{"USR"}}, http.StatusOK, "application/json", ""}, 3262 {"/v4", http.Header{}, http.StatusUnauthorized, "text/html", "access control error: ba1: credentials required"}, 3263 } { 3264 t.Run(tc.path[1:], func(subT *testing.T) { 3265 helper := test.New(subT) 3266 hook.Reset() 3267 3268 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil) 3269 helper.Must(err) 3270 3271 if val := tc.header.Get("Authorization"); val != "" { 3272 req.Header.Set("Authorization", val) 3273 } 3274 3275 res, err := client.Do(req) 3276 helper.Must(err) 3277 // t.Errorf(">>> %#v", res.Header) 3278 3279 message := getFirstAccessLogMessage(hook) 3280 if tc.wantErrLog == "" { 3281 if message != "" { 3282 subT.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message) 3283 } 3284 } else { 3285 if message != tc.wantErrLog { 3286 subT.Errorf("Expected error log message: %q, actual: %#v", tc.wantErrLog, message) 3287 } 3288 } 3289 3290 if res.StatusCode != tc.status { 3291 subT.Fatalf("%q: expected Status %d, got: %d", tc.path, tc.status, res.StatusCode) 3292 } 3293 3294 if ct := res.Header.Get("Content-Type"); ct != tc.ct { 3295 subT.Fatalf("%q: expected content-type: %q, got: %q", tc.path, tc.ct, ct) 3296 } 3297 3298 if tc.ct == "text/html" { 3299 return 3300 } 3301 3302 b, err := io.ReadAll(res.Body) 3303 helper.Must(err) 3304 3305 type payload struct { 3306 Headers http.Header 3307 } 3308 var p payload 3309 helper.Must(json.Unmarshal(b, &p)) 3310 3311 for k, v := range tc.header { 3312 if _, ok := p.Headers[k]; !ok { 3313 subT.Errorf("Expected header %q, got nothing", k) 3314 break 3315 } 3316 if !reflect.DeepEqual(p.Headers[k], v) { 3317 subT.Errorf("Expected header %q value: %v, got: %v", k, v, p.Headers[k]) 3318 } 3319 } 3320 }) 3321 } 3322 } 3323 3324 func TestAPICatchAll(t *testing.T) { 3325 client := newClient() 3326 3327 shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t)) 3328 defer shutdown() 3329 3330 type testCase struct { 3331 name string 3332 path string 3333 method string 3334 header http.Header 3335 status int 3336 wantErrLog string 3337 } 3338 3339 for _, tc := range []testCase{ 3340 {"exists, authorized", "/v5/exists", http.MethodGet, http.Header{"Authorization": []string{"Basic OmFzZGY="}}, http.StatusOK, ""}, 3341 {"exists, unauthorized", "/v5/exists", http.MethodGet, http.Header{}, http.StatusUnauthorized, "access control error: ba1: credentials required"}, 3342 {"exists, CORS pre-flight", "/v5/exists", http.MethodOptions, http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}}, http.StatusNoContent, ""}, 3343 {"not-exist, authorized", "/v5/not-exist", http.MethodGet, http.Header{"Authorization": []string{"Basic OmFzZGY="}}, http.StatusNotFound, "route not found error"}, 3344 {"not-exist, unauthorized", "/v5/not-exist", http.MethodGet, http.Header{}, http.StatusUnauthorized, "access control error: ba1: credentials required"}, 3345 {"not-exist, non-standard method, authorized", "/v5/not-exist", "BREW", http.Header{"Authorization": []string{"Basic OmFzZGY="}}, http.StatusMethodNotAllowed, "method not allowed error"}, 3346 {"not-exist, non-standard method, unauthorized", "/v5/not-exist", "BREW", http.Header{}, http.StatusUnauthorized, "access control error: ba1: credentials required"}, 3347 {"not-exist, CORS pre-flight", "/v5/not-exist", http.MethodOptions, http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}}, http.StatusNoContent, ""}, 3348 } { 3349 t.Run(tc.name, func(subT *testing.T) { 3350 helper := test.New(subT) 3351 hook.Reset() 3352 3353 req, err := http.NewRequest(tc.method, "http://back.end:8080"+tc.path, nil) 3354 helper.Must(err) 3355 3356 req.Header = tc.header 3357 3358 res, err := client.Do(req) 3359 helper.Must(err) 3360 3361 message := getFirstAccessLogMessage(hook) 3362 if tc.wantErrLog == "" { 3363 if message != "" { 3364 subT.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message) 3365 } 3366 } else { 3367 if message != tc.wantErrLog { 3368 subT.Errorf("Expected error log message: %q, actual: %#v", tc.wantErrLog, message) 3369 } 3370 } 3371 3372 if res.StatusCode != tc.status { 3373 subT.Fatalf("%q: expected Status %d, got: %d", tc.path, tc.status, res.StatusCode) 3374 } 3375 }) 3376 } 3377 } 3378 3379 func Test_LoadAccessControl(t *testing.T) { 3380 // Tests the config load with ACs and "error_handler" blocks... 3381 backend := test.NewBackend() 3382 defer backend.Close() 3383 3384 shutdown, _, err := newCouperWithTemplate("testdata/integration/config/07_couper.hcl", test.New(t), map[string]interface{}{ 3385 "asOrigin": backend.Addr(), 3386 }) 3387 if err != nil { 3388 t.Fatal(err) 3389 } 3390 3391 test.WaitForOpenPort(8080) 3392 shutdown() 3393 } 3394 3395 func TestJWTAccessControl(t *testing.T) { 3396 client := newClient() 3397 3398 shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t)) 3399 defer shutdown() 3400 3401 type testCase struct { 3402 name string 3403 path string 3404 header http.Header 3405 body string 3406 status int 3407 expPerm string 3408 wantErrLog string 3409 } 3410 3411 tokenRequest, reqerr := http.NewRequest(http.MethodGet, "http://back.end:8080/jwt/create?type=ECDSAToken", nil) 3412 if reqerr != nil { 3413 t.Fatal(reqerr) 3414 } 3415 tokenResponse, resperr := client.Do(tokenRequest) 3416 if reqerr != nil { 3417 t.Fatal(resperr) 3418 } 3419 bytes, _ := io.ReadAll(tokenResponse.Body) 3420 localToken := string(bytes) 3421 3422 // RSA tokens created with server/testdata/integration/files/pkcs8.key 3423 // ECDSA tokens created with server/testdata/integration/files/ecdsa.key 3424 rsaToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.AZ0gZVqPe9TjjjJO0GnlTvERBXhPyxW_gTn050rCoEkseFRlp4TYry7WTQ7J4HNrH3btfxaEQLtTv7KooVLXQyMDujQbKU6cyuYH6MZXaM0Co3Bhu0awoX-2GVk997-7kMZx2yvwIR5ypd1CERIbNs5QcQaI4sqx_8oGrjO5ZmOWRqSpi4Mb8gJEVVccxurPu65gPFq9esVWwTf4cMQ3GGzijatnGDbRWs_igVGf8IAfmiROSVd17fShQtfthOFd19TGUswVAleOftC7-DDeJgAK8Un5xOHGRjv3ypK_6ZLRonhswaGXxovE0kLq4ZSzumQY2hOFE6x_BbrR1WKtGw" 3425 hmacToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGUiOiJmb28gYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.7wz7Z7IajfEpwYayfshag6tQVS0e0zZJyjAhuFC0L-E" 3426 ecdsaToken := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVzMjU2In0.eyJzdWIiOjEyMzQ1Njc4OTB9.jXsNtPUXxBi8Bz2i2Maj9lzbB1ebQDmz8TU6GSs6G0yzq9YguXm_HQuwsg4ZTbPER3bpXH_cxz9eEZHUBXfWzw" 3427 ecdsaToken2 := "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImVzMjU2LWNydi14LXkifQ.eyJzdWIiOjEyMzQ1Njc4OTB9.4-uNC6KGkSY1YYAmGoR-naUu2-Rxo6HSzEkecb7Ua9FVkif0X2gC55DpPU06_HH-yfK-dFozLwzuV2AT6ouOIg" 3428 3429 for _, tc := range []testCase{ 3430 {"no token", "/jwt", http.Header{}, "", http.StatusUnauthorized, "", "access control error: JWTToken: token required"}, 3431 {"expired token", "/jwt", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjEyMzQ1Njc4OSwic2NvcGUiOlsiZm9vIiwiYmFyIl19.W2ziH_V33JkOA5ttQhzWN96RqxFydmx7GHY6G__U9HM"}}, "", http.StatusUnauthorized, "", "access control error: JWTToken: Token is expired"}, 3432 {"valid token", "/jwt", http.Header{"Authorization": []string{"Bearer " + hmacToken}}, "", http.StatusOK, `["foo","bar"]`, ""}, 3433 {"RSA JWT", "/jwt/rsa", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3434 {"RSA JWT PKCS1", "/jwt/rsa/pkcs1", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3435 {"RSA JWT PKCS8", "/jwt/rsa/pkcs8", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3436 {"RSA JWT bad algorithm", "/jwt/rsa/bad", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusUnauthorized, "", "access control error: RSATokenWrongAlgorithm: signing method RS256 is invalid"}, 3437 {"local RSA JWKS without kid", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEyMzQ1Njc4OTB9.V9skZUql-mHqwOzVdzamqAOWSx8fjEA-6py0nfxLRSl7h1bQvqUCWMZUAkMJK6RuJ3y5YAr8ZBXZsh4rwABp_3hitQitMXnV6nr5qfzVDE9-mdS4--Bj46-JlkHacNcK24qlnn_EXGJlzCj6VFgjObSy6geaTY9iDVF6EzjZkxc1H75XRlNYAMu-0KCGfKdte0qASeBKrWnoFNEpnXZ_jhqRRNVkaSBj7_HPXD6oPqKBQf6Jh6fGgdz6q4KNL-t-Qa2_eKc8tkrYNdTdxco-ufmmLiUQ_MzRAqowHb2LdsFJP9rN2QT8MGjRXqGvkCd0EsLfqAeCPkTXs1kN8LGlvw"}}, "", http.StatusUnauthorized, "", `access control error: JWKS: no matching RS256 JWK for kid ""`}, 3438 {"local RSA JWKS with unsupported kid", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer eyJraWQiOiJyczI1Ni11bnN1cHBvcnRlZCIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.wx1MkMgJhh6gnOvvrnnkRpEUDe-0KpKWw9ZIfDVHtGkuL46AktBgfbaW1ttB78wWrIW9OPfpLqKwkPizwfShoXKF9qN-6TlhPSWIUh0_kBHEj7H4u45YZXH1Ha-r9kGzly1PmLx7gzxUqRpqYnwo0TzZSEr_a8rpfWaC0ZJl3CKARormeF3tzW_ARHnGUqck4VjPfX50Ot6B5nool6qmsCQLLmDECIKBDzZicqdeWH7JPvRZx45R5ZHJRQpD3Z2iqVIF177Wj1C8q75Gxj2PXziIVKplmIUrKN-elYj3kBtJkDFneb384FPLuzsQZOR6HQmKXG2nA1WOfsblJSz3FA"}}, "", http.StatusUnauthorized, "", `access control error: JWKS: no matching RS256 JWK for kid "rs256-unsupported"`}, 3439 {"local RSA JWKS with non-parsable cert", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer eyJraWQiOiJyczI1Ni13cm9uZy1jZXJ0IiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOjEyMzQ1Njc4OTB9.n--6mjzfnPKbaYAquBK3v6gsbmvEofSprk3jwWGSKPdDt2VpVOe8ZNtGhJj_3f1h86-wg-gEQT5GhJmsI47X9MJ70j74dqhXUF6w4782OljstP955whuSM9hJAIvUw_WV1sqtkiESA-CZiNJIBydL5YzV2nO3gfEYdy9EdMJ2ykGLRBajRxhShxsfaZykFKvvWpy1LbUc-gfRZ4q8Hs9B7b_9RGdbpRwBtwiqPPzhjC5O86vk7ZoiG9Gq7pg52yEkLqdN4a5QkfP8nNeTTMAsqPQL1-1TAC7rIGekoUtoINRR-cewPpZ_E7JVxXvBVvPe3gX_2NzGtXkLg5QDt6RzQ"}}, "", http.StatusUnauthorized, "", `access control error: JWKS: no matching RS256 JWK for kid "rs256-wrong-cert"`}, 3440 {"local RSA JWKS not found", "/jwks/rsa/not_found", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusUnauthorized, "", `access control error: JWKS_not_found: received no valid JWKs data: <nil>, status code 404`}, 3441 {"local RSA JWKS", "/jwks/rsa", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3442 {"local RSA JWKS with scope", "/jwks/rsa/scope", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTAsInNjb3BlIjpbImZvbyIsImJhciJdfQ.IFqIF_9ELXl3A-oy52G0Sg5f34ah3araOxFboskEw110nXdb_-UuxCnG0naFVFje7xvNrGbJgVAbBRX1v1I_to4BR8RzvIh2hi5IgBmqclIYsYbVWlEhsvjBhFR2b90Rz0APUdfgHp-nvgLB13jxm8f4TRr4ZDnvUQdZp3vI5PMj9optEmlZvexkNLDQLrBvoGCfVHodZyPQMLNVKp0TXWksPT-bw0E7Lq1GeYe2eU0GwHx8fugo2-v44dfCp0RXYYG6bI_Z-U3KZpvdj05n2_UDgTJFFm4c5i9UjILvlO73QJpMNi5eBjerm2alTisSCoiCtfgIgVsM8yHoomgarg"}}, "", http.StatusOK, `["foo","bar"]`, ""}, 3443 {"remote RSA JWKS x5c", "/jwks/rsa/remote", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3444 {"remote RSA JWKS x5c w/ backend", "/jwks/rsa/backend", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3445 {"remote RSA JWKS x5c w/ backendref", "/jwks/rsa/backendref", http.Header{"Authorization": []string{"Bearer " + rsaToken}}, "", http.StatusOK, "", ""}, 3446 {"remote RSA JWKS n, e", "/jwks/rsa/remote", http.Header{"Authorization": []string{"Bearer eyJraWQiOiJyczI1Ni1uZSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.aGOhlWQIZvnwoEZGDBYhkkEduIVa59G57x88L3fiLc1MuWbYS84nHEZnlPDuVJ3_BxdXr6-nZ8gpk1C9vfamDzkbvzbdcJ2FzmvAONm1II3_u5OTc6ZtpREDx9ohlIvkcOcalOUhQLqU5r2uik2bGSVV3vFDbqxQeuNzh49i3VgdtwoaryNYSzbg_Ki8dHiaFrWH-r2WCU08utqpFmNdr8oNw4Y5AYJdUW2aItxDbwJ6YLBJN0_6EApbXsNqiaNXkLws3cxMvczGKODyGGVCPENa-VmTQ41HxsXB-_rMmcnMw3_MjyIueWcjeP8BNvLYt1bKFWdU0NcYCkXvEqE4-g"}}, "", http.StatusOK, "", ""}, 3447 {"token_value query", "/jwt/token_value_query?token=" + hmacToken, http.Header{}, "", http.StatusOK, `["foo","bar"]`, ""}, 3448 {"token_value body", "/jwt/token_value_body", http.Header{"Content-Type": {"application/json"}}, `{"token":"` + hmacToken + `"}`, http.StatusOK, `["foo","bar"]`, ""}, 3449 {"ECDSA JWT", "/jwt/ecdsa", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusOK, "", ""}, 3450 {"ECDSA local JWT", "/jwt/ecdsa", http.Header{"Authorization": []string{"Bearer " + localToken}}, "", http.StatusOK, "", ""}, 3451 {"ECDSA JWT PKCS8", "/jwt/ecdsa8", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusOK, "", ""}, 3452 {"ECDSA JWT bad algorithm", "/jwt/ecdsa/bad", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusUnauthorized, "", "access control error: ECDSATokenWrongAlgorithm: signing method ES256 is invalid"}, 3453 {"ECDSA JWKS with certificate: kid=es256", "/jwks/ecdsa", http.Header{"Authorization": []string{"Bearer " + ecdsaToken}}, "", http.StatusOK, "", ""}, 3454 {"ECDSA JWKS with crv/x/y: kid=es256-crv-x-y", "/jwks/ecdsa", http.Header{"Authorization": []string{"Bearer " + ecdsaToken2}}, "", http.StatusOK, "", ""}, 3455 } { 3456 t.Run(tc.name, func(subT *testing.T) { 3457 helper := test.New(subT) 3458 hook.Reset() 3459 3460 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, strings.NewReader(tc.body)) 3461 helper.Must(err) 3462 3463 req.Header = tc.header 3464 3465 res, err := client.Do(req) 3466 helper.Must(err) 3467 3468 message := getFirstAccessLogMessage(hook) 3469 if res.StatusCode != tc.status { 3470 subT.Errorf("expected Status %d, got: %d (%s)", tc.status, res.StatusCode, message) 3471 return 3472 } 3473 3474 if tc.wantErrLog == "" { 3475 if message != "" { 3476 subT.Errorf("Expected error log: %q, actual: %#v", tc.wantErrLog, message) 3477 } 3478 } else { 3479 if !strings.HasPrefix(message, tc.wantErrLog) { 3480 subT.Errorf("Expected error log message: '%s', actual: '%s'", tc.wantErrLog, message) 3481 } 3482 } 3483 3484 if res.StatusCode != http.StatusOK { 3485 return 3486 } 3487 3488 expSub := "1234567890" 3489 if sub := res.Header.Get("X-Jwt-Sub"); sub != expSub { 3490 subT.Errorf("expected sub: %q, actual: %q", expSub, sub) 3491 return 3492 } 3493 3494 if grantedPermissions := res.Header.Get("X-Granted-Permissions"); grantedPermissions != tc.expPerm { 3495 subT.Errorf("expected granted permissions: %q, actual: %q", tc.expPerm, grantedPermissions) 3496 return 3497 } 3498 }) 3499 } 3500 } 3501 3502 func TestJWT_DefaultErrorHandler(t *testing.T) { 3503 client := newClient() 3504 3505 shutdown, hook := newCouper("testdata/integration/config/03_couper.hcl", test.New(t)) 3506 defer shutdown() 3507 3508 type testCase struct { 3509 name string 3510 path string 3511 header http.Header 3512 status int 3513 wantErrType string 3514 wantWwwAuth string 3515 } 3516 3517 validToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGUiOiJmb28gYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.7wz7Z7IajfEpwYayfshag6tQVS0e0zZJyjAhuFC0L-E" 3518 expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjEyMzQ1Njc4OSwic2NvcGUiOlsiZm9vIiwiYmFyIl19.W2ziH_V33JkOA5ttQhzWN96RqxFydmx7GHY6G__U9HM" 3519 invalidToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGUiOiJmb28gYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.7wz7Z7IajfEpwYayfshag6tQVS0e0" 3520 3521 for _, tc := range []testCase{ 3522 {"valid token", "/jwt", http.Header{"Authorization": []string{"Bearer " + validToken}}, http.StatusOK, "", ""}, 3523 {"no token", "/jwt", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", `Bearer`}, 3524 {"expired token", "/jwt", http.Header{"Authorization": []string{"Bearer " + expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", `Bearer error="invalid_token", error_description="The access token expired"`}, 3525 {"invalid token", "/jwt", http.Header{"Authorization": []string{"Bearer " + invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", `Bearer error="invalid_token"`}, 3526 3527 {"valid token in header", "/jwt/header", http.Header{"X-Token": []string{validToken}}, http.StatusOK, "", ""}, 3528 {"no token in header", "/jwt/header", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", ""}, 3529 {"expired token in header", "/jwt/header", http.Header{"X-Token": []string{expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", ""}, 3530 {"invalid token in header", "/jwt/header", http.Header{"X-Token": []string{invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", ""}, 3531 3532 {"valid token in authorization header", "/jwt/header/auth", http.Header{"Authorization": []string{"Bearer " + validToken}}, http.StatusOK, "", ""}, 3533 {"no token in authorization header", "/jwt/header/auth", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", `Bearer`}, 3534 {"expired token in authorization header", "/jwt/header/auth", http.Header{"Authorization": []string{"Bearer " + expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", `Bearer error="invalid_token", error_description="The access token expired"`}, 3535 {"invalid token in authorization header", "/jwt/header/auth", http.Header{"Authorization": []string{"Bearer " + invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", `Bearer error="invalid_token"`}, 3536 3537 {"valid token in cookie", "/jwt/cookie", http.Header{"Cookie": []string{"tok=" + validToken}}, http.StatusOK, "", ""}, 3538 {"no token in cookie", "/jwt/cookie", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", ""}, 3539 {"expired token in cookie", "/jwt/cookie", http.Header{"Cookie": []string{"tok=" + expiredToken}}, http.StatusUnauthorized, "jwt_token_expired", ""}, 3540 {"invalid token in cookie", "/jwt/cookie", http.Header{"Cookie": []string{"tok=" + invalidToken}}, http.StatusUnauthorized, "jwt_token_invalid", ""}, 3541 3542 {"valid token from token_value", "/jwt/tokenValue?tok=" + validToken, http.Header{}, http.StatusOK, "", ""}, 3543 {"no token from token_value", "/jwt/tokenValue", http.Header{}, http.StatusUnauthorized, "jwt_token_missing", ""}, 3544 {"expired token from token_value", "/jwt/tokenValue?tok=" + expiredToken, http.Header{}, http.StatusUnauthorized, "jwt_token_expired", ""}, 3545 {"invalid token from token_value", "/jwt/tokenValue?tok=" + invalidToken, http.Header{}, http.StatusUnauthorized, "jwt_token_invalid", ""}, 3546 } { 3547 t.Run(tc.name, func(subT *testing.T) { 3548 helper := test.New(subT) 3549 hook.Reset() 3550 3551 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil) 3552 helper.Must(err) 3553 3554 req.Header = tc.header 3555 3556 res, err := client.Do(req) 3557 helper.Must(err) 3558 3559 if res.StatusCode != tc.status { 3560 subT.Errorf("expected Status %d, got: %d", tc.status, res.StatusCode) 3561 return 3562 } 3563 3564 errorType := getAccessLogErrorType(hook) 3565 if errorType != tc.wantErrType { 3566 subT.Errorf("Expected error type: %q, actual: %q", tc.wantErrType, errorType) 3567 } 3568 3569 wwwAuth := res.Header.Get("WWW-Authenticate") 3570 if wwwAuth != tc.wantWwwAuth { 3571 subT.Errorf("Expected www-authenticate: %q, actual: %q", tc.wantWwwAuth, wwwAuth) 3572 } 3573 }) 3574 } 3575 } 3576 3577 func TestJWKsMaxStale(t *testing.T) { 3578 helper := test.New(t) 3579 client := newClient() 3580 3581 config := ` 3582 server { 3583 endpoint "/" { 3584 access_control = ["stale"] 3585 response { 3586 body = "hi" 3587 } 3588 } 3589 } 3590 definitions { 3591 jwt "stale" { 3592 jwks_url = "${env.COUPER_TEST_BACKEND_ADDR}/jwks.json" 3593 jwks_ttl = "3s" 3594 jwks_max_stale = "2s" 3595 backend { 3596 origin = env.COUPER_TEST_BACKEND_ADDR 3597 set_request_headers = { 3598 Self-Destruct: ` + fmt.Sprint(time.Now().Add(2*time.Second).Unix()) + ` 3599 } 3600 } 3601 } 3602 } 3603 ` 3604 3605 shutdown, hook, err := newCouperWithBytes([]byte(config), helper) 3606 defer shutdown() 3607 helper.Must(err) 3608 3609 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080/", nil) 3610 helper.Must(err) 3611 3612 rsaToken := "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2IiwidHlwIjoiSldUIn0.eyJzdWIiOjEyMzQ1Njc4OTB9.AZ0gZVqPe9TjjjJO0GnlTvERBXhPyxW_gTn050rCoEkseFRlp4TYry7WTQ7J4HNrH3btfxaEQLtTv7KooVLXQyMDujQbKU6cyuYH6MZXaM0Co3Bhu0awoX-2GVk997-7kMZx2yvwIR5ypd1CERIbNs5QcQaI4sqx_8oGrjO5ZmOWRqSpi4Mb8gJEVVccxurPu65gPFq9esVWwTf4cMQ3GGzijatnGDbRWs_igVGf8IAfmiROSVd17fShQtfthOFd19TGUswVAleOftC7-DDeJgAK8Un5xOHGRjv3ypK_6ZLRonhswaGXxovE0kLq4ZSzumQY2hOFE6x_BbrR1WKtGw" 3613 3614 req.Header = http.Header{"Authorization": []string{"Bearer " + rsaToken}} 3615 3616 res, err := client.Do(req) 3617 helper.Must(err) 3618 if res.StatusCode != http.StatusOK { 3619 message := getFirstAccessLogMessage(hook) 3620 t.Fatalf("expected status %d, got: %d (%s)", http.StatusOK, res.StatusCode, message) 3621 } 3622 3623 time.Sleep(3 * time.Second) 3624 // TTL 3s, backend is already failing, responds with stale JWKS 3625 3626 res, err = client.Do(req) 3627 helper.Must(err) 3628 if res.StatusCode != http.StatusOK { 3629 message := getFirstAccessLogMessage(hook) 3630 t.Fatalf("expected status %d, got: %d (%s)", http.StatusOK, res.StatusCode, message) 3631 } 3632 3633 time.Sleep(3 * time.Second) 3634 // stale time (2s) exhausted -> 403 3635 res, err = client.Do(req) 3636 helper.Must(err) 3637 3638 time.Sleep(time.Second) 3639 message := getFirstAccessLogMessage(hook) 3640 if res.StatusCode != http.StatusUnauthorized { 3641 t.Fatalf("expected status %d, got: %d (%s)", http.StatusUnauthorized, res.StatusCode, message) 3642 } 3643 3644 expectedMessage := "access control error: stale: received no valid JWKs data: <nil>, status code 500" 3645 if message != expectedMessage { 3646 t.Fatalf("expected message %q, got: %q", expectedMessage, message) 3647 } 3648 } 3649 3650 func TestJWTAccessControlSourceConfig(t *testing.T) { 3651 helper := test.New(t) 3652 couperConfig, err := configload.LoadFile("testdata/integration/config/05_couper.hcl", "") 3653 helper.Must(err) 3654 3655 log, _ := logrustest.NewNullLogger() 3656 ctx := context.TODO() 3657 3658 expectedMsg := "configuration error: invalid-source: token source is invalid" 3659 3660 err = command.NewRun(ctx).Execute(nil, couperConfig, log.WithContext(ctx)) 3661 logErr, _ := err.(errors.GoError) 3662 if logErr == nil { 3663 t.Error("logErr should not be nil") 3664 } else if logErr.LogError() != expectedMsg { 3665 t.Errorf("\nwant:\t%s\ngot:\t%v", expectedMsg, logErr.LogError()) 3666 } 3667 } 3668 3669 func TestJWTAccessControl_round(t *testing.T) { 3670 pid := "asdf" 3671 client := newClient() 3672 3673 shutdown, hook := newCouper("testdata/integration/config/08_couper.hcl", test.New(t)) 3674 defer shutdown() 3675 3676 type testCase struct { 3677 name string 3678 path string 3679 expGroups []interface{} 3680 } 3681 3682 for _, tc := range []testCase{ 3683 {"separate jwt_signing_profile/jwt", "/separate", []interface{}{"g1", "g2"}}, 3684 {"self-signed jwt", "/self-signed", []interface{}{}}, 3685 } { 3686 t.Run(tc.path, func(subT *testing.T) { 3687 helper := test.New(subT) 3688 hook.Reset() 3689 3690 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://back.end:8080%s/%s/create-jwt", tc.path, pid), nil) 3691 helper.Must(err) 3692 3693 res, err := client.Do(req) 3694 helper.Must(err) 3695 3696 if res.StatusCode != http.StatusOK { 3697 subT.Fatalf("%q: token request: unexpected status: %d", tc.name, res.StatusCode) 3698 } 3699 3700 token := res.Header.Get("X-Jwt") 3701 3702 req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://back.end:8080%s/%s/jwt", tc.path, pid), nil) 3703 helper.Must(err) 3704 req.Header.Set("Authorization", "Bearer "+token) 3705 3706 res, err = client.Do(req) 3707 helper.Must(err) 3708 3709 if res.StatusCode != http.StatusOK { 3710 subT.Fatalf("%q: resource request: unexpected status: %d", tc.name, res.StatusCode) 3711 } 3712 3713 decoder := json.NewDecoder(res.Body) 3714 var claims map[string]interface{} 3715 err = decoder.Decode(&claims) 3716 helper.Must(err) 3717 3718 if _, ok := claims["exp"]; !ok { 3719 subT.Fatalf("%q: missing exp claim: %#v", tc.name, claims) 3720 } 3721 issclaim, ok := claims["iss"] 3722 if !ok { 3723 subT.Fatalf("%q: missing iss claim: %#v", tc.name, claims) 3724 } 3725 if issclaim != "the_issuer" { 3726 subT.Fatalf("%q: unexpected iss claim: %q", tc.name, issclaim) 3727 } 3728 pidclaim, ok := claims["pid"] 3729 if !ok { 3730 subT.Fatalf("%q: missing pid claim: %#v", tc.name, claims) 3731 } 3732 if pidclaim != pid { 3733 subT.Fatalf("%q: unexpected pid claim: %q", tc.name, pidclaim) 3734 } 3735 groupsclaim, ok := claims["groups"] 3736 if !ok { 3737 subT.Fatalf("%q: missing groups claim: %#v", tc.name, claims) 3738 } 3739 groupsclaimArray, ok := groupsclaim.([]interface{}) 3740 if !ok { 3741 subT.Fatalf("%q: groups must be array: %#v", tc.name, groupsclaim) 3742 } 3743 if !cmp.Equal(tc.expGroups, groupsclaimArray) { 3744 subT.Errorf(cmp.Diff(tc.expGroups, groupsclaimArray)) 3745 } 3746 }) 3747 } 3748 } 3749 3750 func TestJWT_CacheControl_private(t *testing.T) { 3751 token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.qSLnmYgnkcOjxlOjFhUHQpCfTQ5elzKY3Mq6gRVT4iI" 3752 client := newClient() 3753 3754 shutdown, hook := newCouper("testdata/integration/config/10_couper.hcl", test.New(t)) 3755 defer shutdown() 3756 3757 var noCC []string 3758 3759 type testCase struct { 3760 name string 3761 path string 3762 setToken bool 3763 expStatus int 3764 expCC []string 3765 } 3766 3767 for _, tc := range []testCase{ 3768 {"no token; no cc from ep", "/cc-private/no-cc", false, 401, []string{"private"}}, 3769 {"no token; cc public from ep", "/cc-private/cc-public", false, 401, []string{"private"}}, 3770 {"no token; no cc from ep; disable", "/no-cc-private/no-cc", false, 401, noCC}, 3771 {"no token; cc public from ep; disable", "/no-cc-private/cc-public", false, 401, noCC}, 3772 {"token; no cc from ep", "/cc-private/no-cc", true, 204, []string{"private"}}, 3773 {"token; cc public from ep", "/cc-private/cc-public", true, 204, []string{"private", "public"}}, 3774 {"token; no public cc from ep; disable", "/no-cc-private/no-cc", true, 204, noCC}, 3775 {"token; cc public from ep; disable", "/no-cc-private/cc-public", true, 204, []string{"public"}}, 3776 } { 3777 t.Run(tc.name, func(subT *testing.T) { 3778 helper := test.New(subT) 3779 hook.Reset() 3780 3781 req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil) 3782 helper.Must(err) 3783 if tc.setToken { 3784 req.Header.Set("Authorization", "Bearer "+token) 3785 } 3786 3787 res, err := client.Do(req) 3788 helper.Must(err) 3789 3790 if res.StatusCode != tc.expStatus { 3791 subT.Errorf("expected Status %d, got: %d", tc.expStatus, res.StatusCode) 3792 return 3793 } 3794 3795 cc := res.Header.Values("Cache-Control") 3796 sort.Strings(cc) 3797 3798 if !cmp.Equal(tc.expCC, cc) { 3799 subT.Errorf("%s", cmp.Diff(tc.expCC, cc)) 3800 } 3801 }) 3802 } 3803 } 3804 3805 func getFirstAccessLogMessage(hook *logrustest.Hook) string { 3806 for _, entry := range hook.AllEntries() { 3807 if entry.Data["type"] == "couper_access" && entry.Message != "" { 3808 return entry.Message 3809 } 3810 } 3811 3812 return "" 3813 } 3814 3815 func Test_Permissions(t *testing.T) { 3816 h := test.New(t) 3817 client := newClient() 3818 3819 shutdown, hook := newCouper("testdata/integration/config/09_couper.hcl", test.New(t)) 3820 defer shutdown() 3821 3822 tok := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 3823 "scp": "a", 3824 "rl": "r1", 3825 }) 3826 token, tokenErr := tok.SignedString([]byte("asdf")) 3827 h.Must(tokenErr) 3828 3829 type testCase struct { 3830 name string 3831 method string 3832 path string 3833 authorize bool 3834 status int 3835 wantGranted string 3836 wantRequired string 3837 wantErrLog string 3838 wantErrType string 3839 } 3840 3841 for _, tc := range []testCase{ 3842 {"by scope: unauthorized", http.MethodGet, "/scope/foo", false, http.StatusUnauthorized, ``, ``, "access control error: scoped_jwt: token required", "jwt_token_missing"}, 3843 {"by scope: no permission required by endpoint", http.MethodGet, "/scope/foo", true, http.StatusNoContent, `["a"]`, ``, "", ""}, 3844 {"by scope: permission required by endpoint: insufficient permissions", http.MethodPost, "/scope/foo", true, http.StatusForbidden, ``, ``, `access control error: required permission "foo" not granted`, "insufficient_permissions"}, 3845 {"by scope: method not permitted", http.MethodDelete, "/scope/foo", true, http.StatusMethodNotAllowed, ``, ``, "method not allowed error: method DELETE not allowed by required_permission", ""}, 3846 {"by scope: permission required by endpoint via *: insufficient permissions", http.MethodGet, "/scope/bar", true, http.StatusForbidden, ``, `more`, `access control error: required permission "more" not granted`, "insufficient_permissions"}, 3847 {"by scope: no permission required by endpoint", http.MethodDelete, "/scope/bar", true, http.StatusNoContent, `["a"]`, ``, "", ""}, 3848 {"by scope: required permission expression", http.MethodGet, "/scope/path/a/path", true, http.StatusNoContent, `["a"]`, ``, "", ""}, 3849 {"by scope: required permission object expression (GET)", http.MethodGet, "/scope/object/get", true, http.StatusNoContent, `["a"]`, ``, "", ""}, 3850 {"by scope: required permission object expression (DELETE)", http.MethodDelete, "/scope/object/delete", true, http.StatusForbidden, ``, ``, `access control error: required permission "z" not granted`, "insufficient_permissions"}, 3851 {"by scope: required permission bad expression", http.MethodGet, "/scope/bad/expression", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"}, 3852 {"by scope: required permission bad type number", http.MethodGet, "/scope/bad/type/number", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"}, 3853 {"by scope: required permission bad type boolean", http.MethodGet, "/scope/bad/type/boolean", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"}, 3854 {"by scope: required permission bad type tuple", http.MethodGet, "/scope/bad/type/tuple", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"}, 3855 {"by scope: required permission bad type null", http.MethodGet, "/scope/bad/type/null", true, http.StatusInternalServerError, ``, ``, "expression evaluation error", "evaluation"}, 3856 {"by scope: required permission by api only: insufficient permissions", http.MethodGet, "/scope/permission-from-api", true, http.StatusForbidden, ``, ``, `access control error: required permission "z" not granted`, "insufficient_permissions"}, 3857 {"by role: unauthorized", http.MethodGet, "/role/foo", false, http.StatusUnauthorized, ``, ``, "access control error: roled_jwt: token required", "jwt_token_missing"}, 3858 {"by role: sufficient permission", http.MethodGet, "/role/foo", true, http.StatusNoContent, `["a","b"]`, ``, "", ""}, 3859 {"by role: permission required by endpoint: insufficient permissions", http.MethodPost, "/role/foo", true, http.StatusForbidden, ``, ``, `access control error: required permission "foo" not granted`, "insufficient_permissions"}, 3860 {"by role: method not permitted", http.MethodDelete, "/role/foo", true, http.StatusMethodNotAllowed, ``, ``, "method not allowed error: method DELETE not allowed by required_permission", ""}, 3861 {"by role: permission required by endpoint via *: insufficient permissions", http.MethodGet, "/role/bar", true, http.StatusForbidden, ``, `more`, `access control error: required permission "more" not granted`, "insufficient_permissions"}, 3862 {"by role: no permission required by endpoint", http.MethodDelete, "/role/bar", true, http.StatusNoContent, `["a","b"]`, ``, "", ""}, 3863 {"by scope/role, mapped from scope", http.MethodGet, "/scope_and_role/foo", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""}, 3864 {"by scope/role, mapped scope mapped from role", http.MethodGet, "/scope_and_role/bar", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""}, 3865 {"by scope/role, mapped from scope, map files", http.MethodGet, "/scope_and_role_files/foo", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""}, 3866 {"by scope/role, mapped scope mapped from role, map files", http.MethodGet, "/scope_and_role_files/bar", true, http.StatusNoContent, `["a","b","c","d","e"]`, ``, "", ""}, 3867 } { 3868 t.Run(fmt.Sprintf("%s_%s_%s", tc.name, tc.method, tc.path), func(subT *testing.T) { 3869 helper := test.New(subT) 3870 hook.Reset() 3871 3872 req, err := http.NewRequest(tc.method, "http://back.end:8080"+tc.path, nil) 3873 helper.Must(err) 3874 3875 if tc.authorize { 3876 req.Header.Set("Authorization", "Bearer "+token) 3877 } 3878 3879 res, err := client.Do(req) 3880 helper.Must(err) 3881 3882 if res.StatusCode != tc.status { 3883 subT.Fatalf("expected Status %d, got: %d", tc.status, res.StatusCode) 3884 } 3885 3886 granted := res.Header.Get("x-granted-permissions") 3887 if granted != tc.wantGranted { 3888 subT.Errorf("Expected granted permissions:\nWant:\t%q\nGot:\t%q", tc.wantGranted, granted) 3889 } 3890 3891 required := res.Header.Get("x-required-permission") 3892 if required != tc.wantRequired { 3893 subT.Errorf("Expected required permission:\nWant:\t%q\nGot:\t%q", tc.wantRequired, required) 3894 } 3895 3896 message := getFirstAccessLogMessage(hook) 3897 if !strings.HasPrefix(message, tc.wantErrLog) { 3898 subT.Errorf("Expected error log:\nWant:\t%q\nGot:\t%q", tc.wantErrLog, message) 3899 } 3900 3901 errorType := getAccessLogErrorType(hook) 3902 if errorType != tc.wantErrType { 3903 subT.Errorf("Expected error type: %q, actual: %q", tc.wantErrType, errorType) 3904 } 3905 }) 3906 } 3907 } 3908 3909 func getAccessLogURL(hook *logrustest.Hook) string { 3910 for _, entry := range hook.AllEntries() { 3911 if entry.Data["type"] == "couper_access" && entry.Data["url"] != "" { 3912 if u, ok := entry.Data["url"].(string); ok { 3913 return u 3914 } 3915 } 3916 } 3917 3918 return "" 3919 } 3920 3921 func getAccessLogErrorType(hook *logrustest.Hook) string { 3922 for _, entry := range hook.AllEntries() { 3923 if entry.Data["type"] == "couper_access" && entry.Data["error_type"] != "" { 3924 if errorType, ok := entry.Data["error_type"].(string); ok { 3925 return errorType 3926 } 3927 } 3928 } 3929 3930 return "" 3931 } 3932 3933 func TestWrapperHiJack_WebsocketUpgrade(t *testing.T) { 3934 helper := test.New(t) 3935 shutdown, _ := newCouper("testdata/integration/api/04_couper.hcl", test.New(t)) 3936 defer shutdown() 3937 3938 req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade", nil) 3939 helper.Must(err) 3940 req.Close = false 3941 3942 req.Header.Set("Connection", "upgrade") 3943 req.Header.Set("Upgrade", "websocket") 3944 3945 conn, err := net.Dial("tcp", "127.0.0.1:8080") 3946 helper.Must(err) 3947 defer conn.Close() 3948 3949 helper.Must(req.Write(conn)) 3950 3951 helper.Must(conn.SetDeadline(time.Time{})) 3952 3953 textConn := textproto.NewConn(conn) 3954 _, _, _ = textConn.ReadResponse(http.StatusSwitchingProtocols) // ignore short resp error 3955 header, err := textConn.ReadMIMEHeader() 3956 helper.Must(err) 3957 3958 expectedHeader := textproto.MIMEHeader{ 3959 "Abc": []string{"123"}, 3960 "Connection": []string{"Upgrade"}, 3961 "Couper-Request-Id": header.Values("Couper-Request-Id"), // dynamic 3962 "Server": []string{"couper.io"}, 3963 "Upgrade": []string{"websocket"}, 3964 } 3965 3966 if !reflect.DeepEqual(expectedHeader, header) { 3967 t.Errorf("Want: %v, got: %v", expectedHeader, header) 3968 } 3969 3970 n, err := conn.Write([]byte("ping")) 3971 helper.Must(err) 3972 3973 if n != 4 { 3974 t.Errorf("Expected 4 written bytes for 'ping', got: %d", n) 3975 } 3976 3977 p := make([]byte, 4) 3978 _, err = conn.Read(p) 3979 helper.Must(err) 3980 3981 if !bytes.Equal(p, []byte("pong")) { 3982 t.Errorf("Expected pong answer, got: %q", string(p)) 3983 } 3984 } 3985 3986 func TestWrapperHiJack_WebsocketUpgradeModifier(t *testing.T) { 3987 helper := test.New(t) 3988 shutdown, _ := newCouper("testdata/integration/api/13_couper.hcl", test.New(t)) 3989 defer shutdown() 3990 3991 req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade/ws", bytes.NewBufferString("ws-client-body")) 3992 helper.Must(err) 3993 req.Close = false 3994 3995 req.Header.Set("Connection", "upgrade") 3996 req.Header.Set("Upgrade", "websocket") 3997 3998 conn, err := net.Dial("tcp", "127.0.0.1:8080") 3999 helper.Must(err) 4000 defer conn.Close() 4001 4002 helper.Must(req.Write(conn)) 4003 4004 helper.Must(conn.SetDeadline(time.Time{})) 4005 4006 textConn := textproto.NewConn(conn) 4007 _, _, _ = textConn.ReadResponse(http.StatusSwitchingProtocols) // ignore short resp error 4008 header, err := textConn.ReadMIMEHeader() 4009 helper.Must(err) 4010 4011 expectedHeader := textproto.MIMEHeader{ 4012 "Abc": {"123"}, 4013 "Connection": {"Upgrade"}, 4014 "Couper-Request-Id": header.Values("Couper-Request-Id"), // dynamic 4015 "Echo": {"ECHO"}, 4016 "Server": {"couper.io"}, 4017 "Upgrade": {"websocket"}, 4018 "X-Body": {"ws-client-body"}, 4019 "X-Upgrade-Body": {"ws-client-body"}, 4020 } 4021 4022 if !reflect.DeepEqual(expectedHeader, header) { 4023 t.Errorf(cmp.Diff(expectedHeader, header)) 4024 } 4025 4026 n, err := conn.Write([]byte("ping")) 4027 helper.Must(err) 4028 4029 if n != 4 { 4030 t.Errorf("Expected 4 written bytes for 'ping', got: %d", n) 4031 } 4032 4033 p := make([]byte, 4) 4034 _, err = conn.Read(p) 4035 helper.Must(err) 4036 4037 if !bytes.Equal(p, []byte("pong")) { 4038 t.Errorf("Expected pong answer, got: %q", string(p)) 4039 } 4040 } 4041 4042 func TestWrapperHiJack_WebsocketUpgradeBodyBuffer(t *testing.T) { 4043 helper := test.New(t) 4044 shutdown, _ := newCouper("testdata/integration/api/13_couper.hcl", test.New(t)) 4045 defer shutdown() 4046 4047 req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade/small", bytes.NewBufferString("client-body")) 4048 helper.Must(err) 4049 req.Close = false 4050 4051 conn, err := net.Dial("tcp", "127.0.0.1:8080") 4052 helper.Must(err) 4053 defer conn.Close() 4054 4055 helper.Must(req.Write(conn)) 4056 4057 helper.Must(conn.SetDeadline(time.Time{})) 4058 4059 res, err := http.ReadResponse(bufio.NewReader(conn), req) 4060 helper.Must(err) 4061 4062 if res.StatusCode != http.StatusOK { 4063 t.Errorf("Expected StatusOK, got: %s", res.Status) 4064 } 4065 4066 expectedHeader := http.Header{ 4067 "Content-Length": res.Header.Values("Content-Length"), // dynamic, could change for other tests, unrelated here 4068 "Content-Type": {"text/plain; charset=utf-8"}, 4069 "Couper-Request-Id": res.Header.Values("Couper-Request-Id"), // dynamic 4070 "Date": res.Header.Values("Date"), // dynamic 4071 "Server": {"couper.io"}, 4072 "X-Body": {"client-body"}, 4073 "X-Resp-Body": {"1234567890"}, 4074 } 4075 4076 if !reflect.DeepEqual(expectedHeader, res.Header) { 4077 t.Errorf(cmp.Diff(expectedHeader, res.Header)) 4078 } 4079 } 4080 4081 func TestWrapperHiJack_WebsocketUpgradeTimeout(t *testing.T) { 4082 helper := test.New(t) 4083 shutdown, _ := newCouper("testdata/integration/api/14_couper.hcl", test.New(t)) 4084 defer shutdown() 4085 4086 req, err := http.NewRequest(http.MethodGet, "http://connect.ws:8080/upgrade", nil) 4087 helper.Must(err) 4088 req.Close = false 4089 4090 req.Header.Set("Connection", "upgrade") 4091 req.Header.Set("Upgrade", "websocket") 4092 4093 conn, err := net.Dial("tcp", "127.0.0.1:8080") 4094 helper.Must(err) 4095 defer conn.Close() 4096 4097 helper.Must(req.Write(conn)) 4098 4099 helper.Must(conn.SetDeadline(time.Time{})) 4100 4101 p := make([]byte, 77) 4102 _, err = conn.Read(p) 4103 helper.Must(err) 4104 4105 if !bytes.HasPrefix(p, []byte("HTTP/1.1 504 Gateway Timeout\r\n")) { 4106 t.Errorf("Expected 504 status and related headers, got:\n%q", string(p)) 4107 } 4108 } 4109 4110 func TestAccessControl_Files_SPA(t *testing.T) { 4111 shutdown, _ := newCouper("testdata/file_serving/conf_ac.hcl", test.New(t)) 4112 defer shutdown() 4113 4114 client := newClient() 4115 4116 type testCase struct { 4117 path string 4118 password string 4119 expStatus int 4120 } 4121 4122 for _, tc := range []testCase{ 4123 {"/favicon.ico", "", http.StatusUnauthorized}, 4124 {"/robots.txt", "", http.StatusUnauthorized}, 4125 {"/app", "", http.StatusUnauthorized}, 4126 {"/app/1", "", http.StatusUnauthorized}, 4127 {"/favicon.ico", "hans", http.StatusNotFound}, 4128 {"/robots.txt", "hans", http.StatusOK}, 4129 {"/app", "hans", http.StatusOK}, 4130 {"/app/1", "hans", http.StatusOK}, 4131 } { 4132 t.Run(tc.path[1:], func(subT *testing.T) { 4133 helper := test.New(subT) 4134 4135 req, err := http.NewRequest(http.MethodGet, "http://protect.me:8080"+tc.path, nil) 4136 helper.Must(err) 4137 4138 if tc.password != "" { 4139 req.SetBasicAuth("", tc.password) 4140 } 4141 4142 res, err := client.Do(req) 4143 helper.Must(err) 4144 4145 if res.StatusCode != tc.expStatus { 4146 subT.Errorf("Expected status: %d, got: %d", tc.expStatus, res.StatusCode) 4147 } 4148 }) 4149 } 4150 } 4151 4152 func TestHTTPServer_MultiAPI(t *testing.T) { 4153 client := newClient() 4154 4155 type expectation struct { 4156 Path string 4157 } 4158 4159 type testCase struct { 4160 path string 4161 exp expectation 4162 } 4163 4164 shutdown, _ := newCouper("testdata/integration/api/05_couper.hcl", test.New(t)) 4165 defer shutdown() 4166 4167 for _, tc := range []testCase{ 4168 {"/v1/xxx", expectation{ 4169 Path: "/v1/xxx", 4170 }}, 4171 {"/v2/yyy", expectation{ 4172 Path: "/v2/yyy", 4173 }}, 4174 {"/v3/zzz", expectation{ 4175 Path: "/v3/zzz", 4176 }}, 4177 } { 4178 t.Run(tc.path, func(subT *testing.T) { 4179 helper := test.New(subT) 4180 4181 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 4182 helper.Must(err) 4183 4184 res, err := client.Do(req) 4185 helper.Must(err) 4186 4187 resBytes, err := io.ReadAll(res.Body) 4188 helper.Must(err) 4189 4190 _ = res.Body.Close() 4191 4192 var jsonResult expectation 4193 err = json.Unmarshal(resBytes, &jsonResult) 4194 if err != nil { 4195 subT.Errorf("unmarshal json: %v: got:\n%s", err, string(resBytes)) 4196 } 4197 4198 if !reflect.DeepEqual(jsonResult, tc.exp) { 4199 subT.Errorf("\nwant: \n%#v\ngot: \n%#v\npayload:\n%s", tc.exp, jsonResult, string(resBytes)) 4200 } 4201 }) 4202 } 4203 } 4204 4205 func TestFunctions(t *testing.T) { 4206 client := newClient() 4207 4208 shutdown, _ := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t)) 4209 defer shutdown() 4210 4211 type testCase struct { 4212 name string 4213 path string 4214 header map[string]string 4215 status int 4216 } 4217 4218 for _, tc := range []testCase{ 4219 {"merge", "/v1/merge", map[string]string{"X-Merged-1": "{\"foo\":[1,2]}", "X-Merged-2": "{\"bar\":[3,4]}", "X-Merged-3": "[\"a\",\"b\"]"}, http.StatusOK}, 4220 {"coalesce", "/v1/coalesce?q=a", map[string]string{"X-Coalesce-1": "/v1/coalesce", "X-Coalesce-2": "default", "X-Coalesce-3": "default", "X-Coalesce-4": "default"}, http.StatusOK}, 4221 {"default", "/v1/default?q=a", map[string]string{ 4222 "X-Default-1": "/v1/default", 4223 "X-Default-2": "default", 4224 "X-Default-3": "default", 4225 "X-Default-4": "default", 4226 "X-Default-5": "prefix-default", 4227 "X-Default-6": "default", 4228 "X-Default-7": "default", 4229 "X-Default-8": "default-8", 4230 "X-Default-9": "", 4231 "X-Default-10": "", 4232 "X-Default-11": "0", 4233 "X-Default-12": "", 4234 "X-Default-13": `{"a":1}`, 4235 "X-Default-14": `{"a":1}`, 4236 "X-Default-15": `[1,2]`, 4237 }, http.StatusOK}, 4238 {"contains", "/v1/contains", map[string]string{ 4239 "X-Contains-1": "yes", 4240 "X-Contains-2": "no", 4241 "X-Contains-3": "yes", 4242 "X-Contains-4": "no", 4243 "X-Contains-5": "yes", 4244 "X-Contains-6": "no", 4245 "X-Contains-7": "yes", 4246 "X-Contains-8": "no", 4247 "X-Contains-9": "yes", 4248 "X-Contains-10": "no", 4249 "X-Contains-11": "yes", 4250 }, http.StatusOK}, 4251 {"length", "/v1/length", map[string]string{ 4252 "X-Length-1": "2", 4253 "X-Length-2": "0", 4254 "X-Length-3": "5", 4255 "X-Length-4": "2", 4256 }, http.StatusOK}, 4257 {"join", "/v1/join", map[string]string{ 4258 "X-Join-1": "0-1-a-b-3-c-1.234-true-false", 4259 "X-Join-2": "||", 4260 "X-Join-3": "0-1-2-3-4", 4261 }, http.StatusOK}, 4262 {"keys", "/v1/keys", map[string]string{ 4263 "X-Keys-1": `["a","b","c"]`, 4264 "X-Keys-2": `[]`, 4265 "X-Keys-3": `["couper-request-id","user-agent"]`, 4266 }, http.StatusOK}, 4267 {"set_intersection", "/v1/set_intersection", map[string]string{ 4268 "X-Set_Intersection-1": `[1,3]`, 4269 "X-Set_Intersection-2": `[1,3]`, 4270 "X-Set_Intersection-3": `[1,3]`, 4271 "X-Set_Intersection-4": `[1,3]`, 4272 "X-Set_Intersection-5": `[3]`, 4273 "X-Set_Intersection-6": `[3]`, 4274 "X-Set_Intersection-7": `[]`, 4275 "X-Set_Intersection-8": `[]`, 4276 "X-Set_Intersection-9": `[]`, 4277 "X-Set_Intersection-10": `[]`, 4278 "X-Set_Intersection-11": `[2.2]`, 4279 "X-Set_Intersection-12": `["b","d"]`, 4280 "X-Set_Intersection-13": `[true]`, 4281 "X-Set_Intersection-14": `[{"a":1}]`, 4282 "X-Set_Intersection-15": `[[1,2]]`, 4283 }, http.StatusOK}, 4284 {"lookup", "/v1/lookup", map[string]string{ 4285 "X-Lookup-1": "1", 4286 "X-Lookup-2": "default", 4287 "X-Lookup-3": "Go-http-client/1.1", 4288 "X-Lookup-4": "default", 4289 }, http.StatusOK}, 4290 {"trim", "/v1/trim", map[string]string{ 4291 "X-Trim": "foo \tbar", 4292 }, http.StatusOK}, 4293 } { 4294 t.Run(tc.path[1:], func(subT *testing.T) { 4295 helper := test.New(subT) 4296 4297 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 4298 helper.Must(err) 4299 4300 res, err := client.Do(req) 4301 helper.Must(err) 4302 4303 if res.StatusCode != tc.status { 4304 subT.Fatalf("%q: expected Status %d, got: %d", tc.name, tc.status, res.StatusCode) 4305 } 4306 4307 for k, v := range tc.header { 4308 if v1 := res.Header.Get(k); v1 != v { 4309 subT.Fatalf("%q: unexpected header value for %q: got: %q, want: %q", tc.name, k, v1, v) 4310 } 4311 } 4312 }) 4313 } 4314 } 4315 4316 func TestFunction_to_number(t *testing.T) { 4317 client := newClient() 4318 4319 shutdown, _ := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t)) 4320 defer shutdown() 4321 4322 helper := test.New(t) 4323 4324 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/v1/to_number", nil) 4325 helper.Must(err) 4326 4327 res, err := client.Do(req) 4328 helper.Must(err) 4329 4330 if res.StatusCode != http.StatusOK { 4331 t.Fatalf("expected Status %d, got: %d", http.StatusOK, res.StatusCode) 4332 } 4333 4334 resBytes, err := io.ReadAll(res.Body) 4335 helper.Must(err) 4336 helper.Must(res.Body.Close()) 4337 4338 exp := `{"float-2_34":2.34,"float-_3":0.3,"from-env":3.14159,"int":34,"int-3_":3,"int-3_0":3,"null":null}` 4339 if string(resBytes) != exp { 4340 t.Fatalf("Unexpected result\nwant: %s\n got: %s", exp, string(resBytes)) 4341 } 4342 } 4343 4344 func TestFunction_to_number_errors(t *testing.T) { 4345 client := newClient() 4346 4347 shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t)) 4348 defer shutdown() 4349 4350 wd, werr := os.Getwd() 4351 if werr != nil { 4352 t.Fatal(werr) 4353 } 4354 wd = wd + "/testdata/integration/functions" 4355 4356 type testCase struct { 4357 name string 4358 path string 4359 expMsg string 4360 } 4361 4362 for _, tc := range []testCase{ 4363 {"string", "/v1/to_number/string", wd + `/01_couper.hcl:65,23-28: Invalid function argument; Invalid value for "v" parameter: cannot convert "two" to number; given string must be a decimal representation of a number.`}, 4364 {"bool", "/v1/to_number/bool", wd + `/01_couper.hcl:73,23-27: Invalid function argument; Invalid value for "v" parameter: cannot convert bool to number.`}, 4365 {"tuple", "/v1/to_number/tuple", wd + `/01_couper.hcl:81,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert tuple to number.`}, 4366 {"object", "/v1/to_number/object", wd + `/01_couper.hcl:89,23-24: Invalid function argument; Invalid value for "v" parameter: cannot convert object to number.`}, 4367 } { 4368 t.Run(tc.path[1:], func(subT *testing.T) { 4369 helper := test.New(subT) 4370 4371 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 4372 helper.Must(err) 4373 4374 res, err := client.Do(req) 4375 helper.Must(err) 4376 4377 if res.StatusCode != http.StatusInternalServerError { 4378 subT.Fatalf("%q: expected Status %d, got: %d", tc.name, http.StatusInternalServerError, res.StatusCode) 4379 } 4380 msg := logHook.LastEntry().Message 4381 if msg != tc.expMsg { 4382 subT.Fatalf("%q: expected log message\nwant: %q\ngot: %q", tc.name, tc.expMsg, msg) 4383 } 4384 }) 4385 } 4386 } 4387 4388 func TestFunction_length_errors(t *testing.T) { 4389 client := newClient() 4390 4391 shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t)) 4392 defer shutdown() 4393 4394 wd, werr := os.Getwd() 4395 if werr != nil { 4396 t.Fatal(werr) 4397 } 4398 wd = wd + "/testdata/integration/functions" 4399 4400 type testCase struct { 4401 name string 4402 path string 4403 expMsg string 4404 } 4405 4406 for _, tc := range []testCase{ 4407 {"object", "/v1/length/object", wd + `/01_couper.hcl:126,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`}, 4408 {"string", "/v1/length/string", wd + `/01_couper.hcl:134,19-26: Error in function call; Call to function "length" failed: collection must be a list, a map or a tuple.`}, 4409 {"null", "/v1/length/null", wd + `/01_couper.hcl:142,26-30: Invalid function argument; Invalid value for "collection" parameter: argument must not be null.`}, 4410 } { 4411 t.Run(tc.path[1:], func(subT *testing.T) { 4412 helper := test.New(subT) 4413 4414 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 4415 helper.Must(err) 4416 4417 res, err := client.Do(req) 4418 helper.Must(err) 4419 4420 if res.StatusCode != http.StatusInternalServerError { 4421 subT.Fatalf("%q: expected Status %d, got: %d", tc.name, http.StatusInternalServerError, res.StatusCode) 4422 } 4423 msg := logHook.LastEntry().Message 4424 if msg != tc.expMsg { 4425 subT.Fatalf("%q: expected log message\nwant: %q\ngot: %q", tc.name, tc.expMsg, msg) 4426 } 4427 }) 4428 } 4429 } 4430 4431 func TestFunction_lookup_errors(t *testing.T) { 4432 client := newClient() 4433 4434 shutdown, logHook := newCouper("testdata/integration/functions/01_couper.hcl", test.New(t)) 4435 defer shutdown() 4436 4437 wd, werr := os.Getwd() 4438 if werr != nil { 4439 t.Fatal(werr) 4440 } 4441 wd = wd + "/testdata/integration/functions" 4442 4443 type testCase struct { 4444 name string 4445 path string 4446 expMsg string 4447 } 4448 4449 for _, tc := range []testCase{ 4450 {"null inputMap", "/v1/lookup/inputMap-null", wd + `/01_couper.hcl:203,26-30: Invalid function argument; Invalid value for "inputMap" parameter: argument must not be null.`}, 4451 } { 4452 t.Run(tc.path[1:], func(subT *testing.T) { 4453 helper := test.New(subT) 4454 4455 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil) 4456 helper.Must(err) 4457 4458 res, err := client.Do(req) 4459 helper.Must(err) 4460 4461 if res.StatusCode != http.StatusInternalServerError { 4462 subT.Fatalf("%q: expected Status %d, got: %d", tc.name, http.StatusInternalServerError, res.StatusCode) 4463 } 4464 msg := logHook.LastEntry().Message 4465 if msg != tc.expMsg { 4466 subT.Fatalf("%q: expected log message\nwant: %q\ngot: %q", tc.name, tc.expMsg, msg) 4467 } 4468 }) 4469 } 4470 } 4471 4472 func TestEndpoint_Response(t *testing.T) { 4473 client := newClient() 4474 var redirSeen bool 4475 client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 4476 redirSeen = true 4477 return fmt.Errorf("do not follow") 4478 } 4479 4480 shutdown, logHook := newCouper("testdata/integration/endpoint_eval/17_couper.hcl", test.New(t)) 4481 defer shutdown() 4482 4483 type testCase struct { 4484 path string 4485 expStatusCode int 4486 } 4487 4488 for _, tc := range []testCase{ 4489 {"/200", http.StatusOK}, 4490 {"/200/this-is-my-resp-body", http.StatusOK}, 4491 {"/204", http.StatusNoContent}, 4492 {"/301", http.StatusMovedPermanently}, 4493 } { 4494 t.Run(tc.path[1:], func(subT *testing.T) { 4495 helper := test.New(subT) 4496 4497 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 4498 helper.Must(err) 4499 4500 res, err := client.Do(req) 4501 if tc.expStatusCode == http.StatusMovedPermanently { 4502 if !redirSeen { 4503 subT.Errorf("expected a redirect response") 4504 } 4505 4506 resp := logHook.LastEntry().Data["response"] 4507 fields := resp.(logging.Fields) 4508 headers := fields["headers"].(map[string]string) 4509 if headers["location"] != "https://couper.io/" { 4510 subT.Errorf("expected location header log") 4511 } 4512 } else { 4513 helper.Must(err) 4514 } 4515 4516 resBytes, err := io.ReadAll(res.Body) 4517 helper.Must(err) 4518 helper.Must(res.Body.Close()) 4519 4520 if res.StatusCode != tc.expStatusCode { 4521 subT.Fatalf("%q: expected Status %d, got: %d", tc.path, tc.expStatusCode, res.StatusCode) 4522 } 4523 4524 if logHook.LastEntry().Data["status"] != tc.expStatusCode { 4525 subT.Logf("%v", logHook.LastEntry()) 4526 subT.Errorf("Expected statusCode log: %d", tc.expStatusCode) 4527 } 4528 4529 if len(resBytes) > 0 { 4530 b, exist := logHook.LastEntry().Data["response"].(logging.Fields)["bytes"] 4531 if !exist || b != len(resBytes) { 4532 subT.Errorf("Want bytes log: %d\ngot:\t%v", len(resBytes), logHook.LastEntry()) 4533 } 4534 } 4535 }) 4536 } 4537 } 4538 4539 func TestCORS_Configuration(t *testing.T) { 4540 client := newClient() 4541 4542 shutdown, _ := newCouper("testdata/integration/config/06_couper.hcl", test.New(t)) 4543 defer shutdown() 4544 4545 requestMethod := "GET" 4546 requestHeaders := "Authorization" 4547 4548 type testCase struct { 4549 path string 4550 origin string 4551 expAllowed bool 4552 expAllowedMethods string 4553 expAllowedHeaders string 4554 expVaryPF string 4555 expVary string 4556 expVaryCred string 4557 } 4558 4559 for _, tc := range []testCase{ 4560 {"/06_couper.hcl", "a.com", true, requestMethod, requestHeaders, "Origin,Access-Control-Request-Method,Access-Control-Request-Headers", "Origin,Accept-Encoding", "Origin,Accept-Encoding"}, 4561 {"/spa/", "b.com", true, requestMethod, requestHeaders, "Origin,Access-Control-Request-Method,Access-Control-Request-Headers", "Origin,Accept-Encoding", "Origin,Accept-Encoding"}, 4562 {"/api/", "c.com", true, requestMethod, requestHeaders, "Origin,Access-Control-Request-Method,Access-Control-Request-Headers", "Origin,Accept-Encoding", "Origin"}, 4563 {"/06_couper.hcl", "no.com", false, "", "", "Origin", "Origin,Accept-Encoding", "Origin,Accept-Encoding"}, 4564 {"/spa/", "", false, "", "", "Origin", "Origin,Accept-Encoding", "Origin,Accept-Encoding"}, 4565 {"/api/", "no.com", false, "", "", "Origin", "Origin,Accept-Encoding", "Origin"}, 4566 } { 4567 t.Run(tc.path[1:], func(subT *testing.T) { 4568 helper := test.New(subT) 4569 4570 // preflight request 4571 req, err := http.NewRequest(http.MethodOptions, "http://localhost:8080"+tc.path, nil) 4572 helper.Must(err) 4573 4574 req.Header.Set("Access-Control-Request-Method", requestMethod) 4575 req.Header.Set("Access-Control-Request-Headers", requestHeaders) 4576 req.Header.Set("Origin", tc.origin) 4577 4578 res, err := client.Do(req) 4579 helper.Must(err) 4580 4581 helper.Must(res.Body.Close()) 4582 4583 if res.StatusCode != http.StatusNoContent { 4584 subT.Fatalf("%q: expected Status %d, got: %d", tc.path, http.StatusNoContent, res.StatusCode) 4585 } 4586 4587 acao, acaoExists := res.Header["Access-Control-Allow-Origin"] 4588 acam, acamExists := res.Header["Access-Control-Allow-Methods"] 4589 acah, acahExists := res.Header["Access-Control-Allow-Headers"] 4590 acac, acacExists := res.Header["Access-Control-Allow-Credentials"] 4591 if tc.expAllowed { 4592 if !acaoExists || acao[0] != tc.origin { 4593 subT.Errorf("Expected allowed origin, got: %v", acao) 4594 } 4595 if !acamExists || acam[0] != tc.expAllowedMethods { 4596 subT.Errorf("Expected allowed methods, got: %v", acam) 4597 } 4598 if !acahExists || acah[0] != tc.expAllowedHeaders { 4599 subT.Errorf("Expected allowed headers, got: %v", acah) 4600 } 4601 if !acacExists || acac[0] != "true" { 4602 subT.Errorf("Expected allowed credentials, got: %v", acac) 4603 } 4604 } else { 4605 if acaoExists { 4606 subT.Errorf("Expected not allowed origin, got: %v", acao) 4607 } 4608 if acamExists { 4609 subT.Errorf("Expected not allowed methods, got: %v", acam) 4610 } 4611 if acahExists { 4612 subT.Errorf("Expected not allowed headers, got: %v", acah) 4613 } 4614 if acacExists { 4615 subT.Errorf("Expected not allowed credentials, got: %v", acac) 4616 } 4617 } 4618 vary, varyExists := res.Header["Vary"] 4619 if !varyExists || strings.Join(vary, ",") != tc.expVaryPF { 4620 subT.Errorf("Expected vary %q, got: %q", tc.expVaryPF, strings.Join(vary, ",")) 4621 } 4622 4623 // actual request lacking credentials -> rejected by basic_auth AC 4624 req, err = http.NewRequest(requestMethod, "http://localhost:8080"+tc.path, nil) 4625 helper.Must(err) 4626 4627 req.Header.Set("Origin", tc.origin) 4628 4629 res, err = client.Do(req) 4630 helper.Must(err) 4631 4632 helper.Must(res.Body.Close()) 4633 4634 if res.StatusCode != http.StatusUnauthorized { 4635 subT.Fatalf("%q: expected Status %d, got: %d", tc.path, http.StatusUnauthorized, res.StatusCode) 4636 } 4637 4638 acao, acaoExists = res.Header["Access-Control-Allow-Origin"] 4639 acac, acacExists = res.Header["Access-Control-Allow-Credentials"] 4640 if tc.expAllowed { 4641 if !acaoExists || acao[0] != tc.origin { 4642 subT.Errorf("Expected allowed origin, got: %v", acao) 4643 } 4644 if !acacExists || acac[0] != "true" { 4645 subT.Errorf("Expected allowed credentials, got: %v", acac) 4646 } 4647 } else { 4648 if acaoExists { 4649 subT.Errorf("Expected not allowed origin, got: %v", acao) 4650 } 4651 if acacExists { 4652 subT.Errorf("Expected not allowed credentials, got: %v", acac) 4653 } 4654 } 4655 vary, varyExists = res.Header["Vary"] 4656 if !varyExists || strings.Join(vary, ",") != tc.expVary { 4657 subT.Errorf("Expected vary %q, got: %q", tc.expVary, strings.Join(vary, ",")) 4658 } 4659 4660 // actual request with credentials 4661 req, err = http.NewRequest(requestMethod, "http://localhost:8080"+tc.path, nil) 4662 helper.Must(err) 4663 4664 req.Header.Set("Origin", tc.origin) 4665 req.Header.Set("Authorization", "Basic Zm9vOmFzZGY=") 4666 4667 res, err = client.Do(req) 4668 helper.Must(err) 4669 4670 helper.Must(res.Body.Close()) 4671 4672 if res.StatusCode != http.StatusOK { 4673 subT.Fatalf("%q: expected Status %d, got: %d", tc.path, http.StatusOK, res.StatusCode) 4674 } 4675 4676 acao, acaoExists = res.Header["Access-Control-Allow-Origin"] 4677 acac, acacExists = res.Header["Access-Control-Allow-Credentials"] 4678 if tc.expAllowed { 4679 if !acaoExists || acao[0] != tc.origin { 4680 subT.Errorf("Expected allowed origin, got: %v", acao) 4681 } 4682 if !acacExists || acac[0] != "true" { 4683 subT.Errorf("Expected allowed credentials, got: %v", acac) 4684 } 4685 } else { 4686 if acaoExists { 4687 subT.Errorf("Expected not allowed origin, got: %v", acao) 4688 } 4689 if acacExists { 4690 subT.Errorf("Expected not allowed credentials, got: %v", acac) 4691 } 4692 } 4693 vary, varyExists = res.Header["Vary"] 4694 if !varyExists || strings.Join(vary, ",") != tc.expVaryCred { 4695 subT.Errorf("Expected vary %q, got: %q", tc.expVaryCred, strings.Join(vary, ",")) 4696 } 4697 }) 4698 } 4699 } 4700 4701 func TestLog_Level(t *testing.T) { 4702 shutdown, hook := newCouper("testdata/integration/logs/03_couper.hcl", test.New(t)) 4703 defer shutdown() 4704 4705 client := newClient() 4706 4707 helper := test.New(t) 4708 4709 req, err := http.NewRequest(http.MethodGet, "http://my.upstream:8080/", nil) 4710 helper.Must(err) 4711 4712 hook.Reset() 4713 4714 res, err := client.Do(req) 4715 helper.Must(err) 4716 4717 if res.StatusCode != http.StatusInternalServerError { 4718 t.Errorf("Expected status: %d, got: %d", http.StatusInternalServerError, res.StatusCode) 4719 } 4720 4721 for _, entry := range hook.AllEntries() { 4722 if entry.Level != logrus.InfoLevel { 4723 t.Errorf("Expected info level, got: %v", entry.Level) 4724 } 4725 } 4726 } 4727 4728 func TestOAuthPKCEFunctions(t *testing.T) { 4729 client := newClient() 4730 4731 shutdown, _ := newCouper("testdata/integration/functions/02_couper.hcl", test.New(t)) 4732 defer shutdown() 4733 4734 helper := test.New(t) 4735 4736 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil) 4737 helper.Must(err) 4738 4739 res, err := client.Do(req) 4740 helper.Must(err) 4741 4742 if res.StatusCode != 200 { 4743 t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode) 4744 } 4745 4746 v1 := res.Header.Get("x-v-1") 4747 v2 := res.Header.Get("x-v-2") 4748 hv := res.Header.Get("x-hv") 4749 if v2 != v1 { 4750 t.Errorf("multiple calls to oauth2_verifier() must return the same value:\n\t%s\n\t%s", v1, v2) 4751 } 4752 s256 := oauth2.Base64urlSha256(v1) 4753 if hv != s256 { 4754 t.Errorf("call to internal_oauth_hashed_verifier() returns wrong value:\nactual:\t\t%s\nexpected:\t%s", hv, s256) 4755 } 4756 au, err := url.Parse(res.Header.Get("x-au-pkce")) 4757 helper.Must(err) 4758 auq := au.Query() 4759 if auq.Get("response_type") != "code" { 4760 t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code") 4761 } 4762 if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" { 4763 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback") 4764 } 4765 if auq.Get("scope") != "openid profile email" { 4766 t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile email") 4767 } 4768 if auq.Get("code_challenge_method") != "S256" { 4769 t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "S256") 4770 } 4771 if auq.Get("code_challenge") != hv { 4772 t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), hv) 4773 } 4774 if auq.Get("state") != "" { 4775 t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "") 4776 } 4777 if auq.Get("nonce") != "" { 4778 t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "") 4779 } 4780 if auq.Get("client_id") != "foo" { 4781 t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo") 4782 } 4783 au, err = url.Parse(res.Header.Get("x-au-pkce-rel")) 4784 helper.Must(err) 4785 auq = au.Query() 4786 if auq.Get("redirect_uri") != "http://example.com:8080/oidc/callback" { 4787 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://example.com:8080/oidc/callback") 4788 } 4789 4790 req, err = http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil) 4791 helper.Must(err) 4792 4793 res, err = client.Do(req) 4794 helper.Must(err) 4795 4796 cv1_n := res.Header.Get("x-v-1") 4797 if cv1_n == v1 { 4798 t.Errorf("calls to oauth2_verifier() on different requests must not return the same value:\n\t%s\n\t%s", v1, cv1_n) 4799 } 4800 } 4801 4802 func TestOAuthStateFunctions(t *testing.T) { 4803 client := newClient() 4804 4805 shutdown, _ := newCouper("testdata/integration/functions/02_couper.hcl", test.New(t)) 4806 defer shutdown() 4807 4808 helper := test.New(t) 4809 4810 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/csrf", nil) 4811 helper.Must(err) 4812 4813 res, err := client.Do(req) 4814 helper.Must(err) 4815 4816 if res.StatusCode != 200 { 4817 t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode) 4818 } 4819 4820 hv := res.Header.Get("x-hv") 4821 au, err := url.Parse(res.Header.Get("x-au-state")) 4822 helper.Must(err) 4823 auq := au.Query() 4824 if auq.Get("response_type") != "code" { 4825 t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code") 4826 } 4827 if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" { 4828 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback") 4829 } 4830 if auq.Get("scope") != "openid profile" { 4831 t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile") 4832 } 4833 if auq.Get("code_challenge_method") != "" { 4834 t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "") 4835 } 4836 if auq.Get("code_challenge") != "" { 4837 t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), "") 4838 } 4839 if auq.Get("state") != hv { 4840 t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), hv) 4841 } 4842 if auq.Get("nonce") != "" { 4843 t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "") 4844 } 4845 if auq.Get("client_id") != "foo" { 4846 t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo") 4847 } 4848 } 4849 4850 func TestOIDCPKCEFunctions(t *testing.T) { 4851 client := newClient() 4852 helper := test.New(t) 4853 4854 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 4855 if req.URL.Path == "/.well-known/openid-configuration" { 4856 rw.Header().Set("Content-Type", "Application/json") 4857 body := []byte(`{ 4858 "issuer": "https://authorization.server", 4859 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 4860 "token_endpoint": "http://` + req.Host + `/token", 4861 "jwks_uri": "http://` + req.Host + `/jwks", 4862 "userinfo_endpoint": "http://` + req.Host + `/userinfo" 4863 }`) 4864 _, werr := rw.Write(body) 4865 helper.Must(werr) 4866 return 4867 } else if req.URL.Path == "/jwks" { 4868 rw.Header().Set("Content-Type", "Application/json") 4869 _, werr := rw.Write([]byte(`{}`)) 4870 helper.Must(werr) 4871 return 4872 } 4873 rw.WriteHeader(http.StatusBadRequest) 4874 })) 4875 defer oauthOrigin.Close() 4876 4877 shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL}) 4878 helper.Must(err) 4879 defer shutdown() 4880 4881 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/pkce", nil) 4882 helper.Must(err) 4883 4884 res, err := client.Do(req) 4885 helper.Must(err) 4886 4887 if res.StatusCode != 200 { 4888 t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode) 4889 } 4890 4891 hv := res.Header.Get("x-hv") 4892 au, err := url.Parse(res.Header.Get("x-au-pkce")) 4893 helper.Must(err) 4894 auq := au.Query() 4895 if auq.Get("response_type") != "code" { 4896 t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code") 4897 } 4898 if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" { 4899 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback") 4900 } 4901 if auq.Get("scope") != "openid profile email" { 4902 t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile email") 4903 } 4904 if auq.Get("code_challenge_method") != "S256" { 4905 t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "S256") 4906 } 4907 if auq.Get("code_challenge") != hv { 4908 t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), hv) 4909 } 4910 if auq.Get("state") != "" { 4911 t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "") 4912 } 4913 if auq.Get("nonce") != "" { 4914 t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "") 4915 } 4916 if auq.Get("client_id") != "foo" { 4917 t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo") 4918 } 4919 au, err = url.Parse(res.Header.Get("x-au-pkce-rel")) 4920 helper.Must(err) 4921 auq = au.Query() 4922 if auq.Get("redirect_uri") != "http://example.com:8080/oidc/callback" { 4923 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://example.com:8080/oidc/callback") 4924 } 4925 } 4926 4927 func TestOIDCNonceFunctions(t *testing.T) { 4928 client := newClient() 4929 helper := test.New(t) 4930 4931 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 4932 if req.URL.Path == "/.well-known/openid-configuration" { 4933 body := []byte(`{ 4934 "issuer": "https://authorization.server", 4935 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 4936 "token_endpoint": "http://` + req.Host + `/token", 4937 "jwks_uri": "http://` + req.Host + `/jwks", 4938 "userinfo_endpoint": "http://` + req.Host + `/userinfo" 4939 }`) 4940 _, werr := rw.Write(body) 4941 helper.Must(werr) 4942 4943 return 4944 } else if req.URL.Path == "/jwks" { 4945 rw.Header().Set("Content-Type", "Application/json") 4946 _, werr := rw.Write([]byte(`{}`)) 4947 helper.Must(werr) 4948 return 4949 } 4950 rw.WriteHeader(http.StatusBadRequest) 4951 })) 4952 defer oauthOrigin.Close() 4953 4954 shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL}) 4955 helper.Must(err) 4956 defer shutdown() 4957 4958 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/csrf", nil) 4959 helper.Must(err) 4960 4961 res, err := client.Do(req) 4962 helper.Must(err) 4963 4964 if res.StatusCode != 200 { 4965 t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode) 4966 } 4967 4968 hv := res.Header.Get("x-hv") 4969 au, err := url.Parse(res.Header.Get("x-au-nonce")) 4970 helper.Must(err) 4971 auq := au.Query() 4972 if auq.Get("response_type") != "code" { 4973 t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code") 4974 } 4975 if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" { 4976 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback") 4977 } 4978 if auq.Get("scope") != "openid profile" { 4979 t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile") 4980 } 4981 if auq.Get("code_challenge_method") != "" { 4982 t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "") 4983 } 4984 if auq.Get("code_challenge") != "" { 4985 t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), "") 4986 } 4987 if auq.Get("state") != "" { 4988 t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "") 4989 } 4990 if auq.Get("nonce") != hv { 4991 t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), hv) 4992 } 4993 if auq.Get("client_id") != "foo" { 4994 t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo") 4995 } 4996 } 4997 4998 func TestOIDCDefaultPKCEFunctions(t *testing.T) { 4999 client := newClient() 5000 helper := test.New(t) 5001 5002 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 5003 if req.URL.Path == "/.well-known/openid-configuration" { 5004 body := []byte(`{ 5005 "issuer": "https://authorization.server", 5006 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 5007 "token_endpoint": "http://` + req.Host + `/token", 5008 "jwks_uri": "http://` + req.Host + `/jwks", 5009 "userinfo_endpoint": "http://` + req.Host + `/userinfo", 5010 "code_challenge_methods_supported": ["S256"] 5011 }`) 5012 _, werr := rw.Write(body) 5013 helper.Must(werr) 5014 return 5015 } else if req.URL.Path == "/jwks" { 5016 rw.Header().Set("Content-Type", "Application/json") 5017 _, werr := rw.Write([]byte(`{}`)) 5018 helper.Must(werr) 5019 return 5020 } 5021 5022 rw.WriteHeader(http.StatusBadRequest) 5023 })) 5024 defer oauthOrigin.Close() 5025 5026 shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL}) 5027 helper.Must(err) 5028 defer shutdown() 5029 5030 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/default", nil) 5031 helper.Must(err) 5032 5033 res, err := client.Do(req) 5034 helper.Must(err) 5035 5036 if res.StatusCode != 200 { 5037 t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode) 5038 } 5039 5040 hv := res.Header.Get("x-hv") 5041 au, err := url.Parse(res.Header.Get("x-au-default")) 5042 helper.Must(err) 5043 auq := au.Query() 5044 if auq.Get("response_type") != "code" { 5045 t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code") 5046 } 5047 if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" { 5048 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback") 5049 } 5050 if auq.Get("scope") != "openid profile email address" { 5051 t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile email") 5052 } 5053 if auq.Get("code_challenge_method") != "S256" { 5054 t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "S256") 5055 } 5056 if auq.Get("code_challenge") != hv { 5057 t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), hv) 5058 } 5059 if auq.Get("state") != "" { 5060 t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "") 5061 } 5062 if auq.Get("nonce") != "" { 5063 t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), "") 5064 } 5065 if auq.Get("client_id") != "foo" { 5066 t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo") 5067 } 5068 } 5069 5070 func TestOIDCDefaultNonceFunctions(t *testing.T) { 5071 client := newClient() 5072 helper := test.New(t) 5073 5074 oauthOrigin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 5075 if req.URL.Path == "/.well-known/openid-configuration" { 5076 body := []byte(`{ 5077 "issuer": "https://authorization.server", 5078 "authorization_endpoint": "https://authorization.server/oauth2/authorize", 5079 "token_endpoint": "http://` + req.Host + `/token", 5080 "jwks_uri": "http://` + req.Host + `/jwks", 5081 "userinfo_endpoint": "http://` + req.Host + `/userinfo" 5082 }`) 5083 _, werr := rw.Write(body) 5084 helper.Must(werr) 5085 return 5086 } else if req.URL.Path == "/jwks" { 5087 rw.Header().Set("Content-Type", "Application/json") 5088 _, werr := rw.Write([]byte(`{}`)) 5089 helper.Must(werr) 5090 return 5091 } 5092 rw.WriteHeader(http.StatusBadRequest) 5093 })) 5094 defer oauthOrigin.Close() 5095 5096 shutdown, _, err := newCouperWithTemplate("testdata/integration/functions/03_couper.hcl", test.New(t), map[string]interface{}{"asOrigin": oauthOrigin.URL}) 5097 helper.Must(err) 5098 defer shutdown() 5099 5100 req, err := http.NewRequest(http.MethodGet, "http://example.com:8080/default", nil) 5101 helper.Must(err) 5102 5103 res, err := client.Do(req) 5104 helper.Must(err) 5105 5106 if res.StatusCode != 200 { 5107 t.Fatalf("expected Status %d, got: %d", 200, res.StatusCode) 5108 } 5109 5110 hv := res.Header.Get("x-hv") 5111 au, err := url.Parse(res.Header.Get("x-au-default")) 5112 helper.Must(err) 5113 auq := au.Query() 5114 if auq.Get("response_type") != "code" { 5115 t.Errorf("oauth2_authorization_url(): wrong response_type query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("response_type"), "code") 5116 } 5117 if auq.Get("redirect_uri") != "http://localhost:8085/oidc/callback" { 5118 t.Errorf("oauth2_authorization_url(): wrong redirect_uri query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("redirect_uri"), "http://localhost:8085/oidc/callback") 5119 } 5120 if auq.Get("scope") != "openid profile email address" { 5121 t.Errorf("oauth2_authorization_url(): wrong scope query param:\nactual:\t\t%s\nexpected:\t%s", auq.Get("scope"), "openid profile") 5122 } 5123 if auq.Get("code_challenge_method") != "" { 5124 t.Errorf("oauth2_authorization_url(): wrong code_challenge_method:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge_method"), "") 5125 } 5126 if auq.Get("code_challenge") != "" { 5127 t.Errorf("oauth2_authorization_url(): wrong code_challenge:\nactual:\t\t%s\nexpected:\t%s", auq.Get("code_challenge"), "") 5128 } 5129 if auq.Get("state") != "" { 5130 t.Errorf("oauth2_authorization_url(): wrong state:\nactual:\t\t%s\nexpected:\t%s", auq.Get("state"), "") 5131 } 5132 if auq.Get("nonce") != hv { 5133 t.Errorf("oauth2_authorization_url(): wrong nonce:\nactual:\t\t%s\nexpected:\t%s", auq.Get("nonce"), hv) 5134 } 5135 if auq.Get("client_id") != "foo" { 5136 t.Errorf("oauth2_authorization_url(): wrong client_id:\nactual:\t\t%s\nexpected:\t%s", auq.Get("client_id"), "foo") 5137 } 5138 } 5139 5140 func TestAllowedMethods(t *testing.T) { 5141 client := newClient() 5142 5143 confPath := "testdata/integration/config/11_couper.hcl" 5144 shutdown, logHook := newCouper(confPath, test.New(t)) 5145 defer shutdown() 5146 5147 token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZSI6ImZvbyJ9.7zkwmXTmzFTKHC0Qnpw7uQCcacogWUvi_JU56uWJlkw" 5148 5149 type testCase struct { 5150 name string 5151 method string 5152 path string 5153 requestHeaders http.Header 5154 status int 5155 couperError string 5156 } 5157 5158 for _, tc := range []testCase{ 5159 {"path not found, authorized", http.MethodGet, "/api1/not-found", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusNotFound, "route not found error"}, 5160 5161 {"unrestricted, authorized, GET", http.MethodGet, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5162 {"unrestricted, authorized, HEAD", http.MethodHead, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5163 {"unrestricted, authorized, POST", http.MethodPost, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5164 {"unrestricted, authorized, PUT", http.MethodPut, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5165 {"unrestricted, authorized, PATCH", http.MethodPatch, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5166 {"unrestricted, authorized, DELETE", http.MethodDelete, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5167 {"unrestricted, authorized, OPTIONS", http.MethodOptions, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5168 {"unrestricted, authorized, CONNECT", http.MethodConnect, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5169 {"unrestricted, authorized, TRACE", http.MethodTrace, "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5170 {"unrestricted, authorized, BREW", "BREW", "/api1/unrestricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5171 {"unrestricted, unauthorized, GET", http.MethodGet, "/api1/unrestricted", http.Header{}, http.StatusUnauthorized, "access control error"}, 5172 {"unrestricted, unauthorized, BREW", "BREW", "/api1/unrestricted", http.Header{}, http.StatusUnauthorized, "access control error"}, 5173 {"unrestricted, CORS preflight", http.MethodOptions, "/api1/unrestricted", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""}, 5174 5175 {"restricted, authorized, GET", http.MethodGet, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5176 {"restricted, authorized, HEAD", http.MethodHead, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5177 {"restricted, authorized, POST", http.MethodPost, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5178 {"restricted, authorized, PUT", http.MethodPut, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5179 {"restricted, authorized, PATCH", http.MethodPatch, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5180 {"restricted, authorized, DELETE", http.MethodDelete, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5181 {"restricted, authorized, OPTIONS", http.MethodOptions, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5182 {"restricted, authorized, CONNECT", http.MethodConnect, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5183 {"restricted, authorized, TRACE", http.MethodTrace, "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusMethodNotAllowed, "method not allowed error"}, 5184 {"restricted, authorized, BREW", "BREW", "/api1/restricted", http.Header{"Authorization": []string{"Bearer " + token}}, http.StatusOK, ""}, 5185 {"restricted, CORS preflight", http.MethodOptions, "/api1/restricted", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""}, 5186 5187 {"wildcard, GET", http.MethodGet, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5188 {"wildcard, HEAD", http.MethodHead, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5189 {"wildcard, POST", http.MethodPost, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5190 {"wildcard, PUT", http.MethodPut, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5191 {"wildcard, PATCH", http.MethodPatch, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5192 {"wildcard, DELETE", http.MethodDelete, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5193 {"wildcard, OPTIONS", http.MethodOptions, "/api1/wildcard", http.Header{}, http.StatusOK, ""}, 5194 {"wildcard, CONNECT", http.MethodConnect, "/api1/wildcard", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5195 {"wildcard, TRACE", http.MethodTrace, "/api1/wildcard", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5196 {"wildcard, BREW", "BREW", "/api1/wildcard", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5197 5198 {"wildcard and more, GET", http.MethodGet, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5199 {"wildcard and more, HEAD", http.MethodHead, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5200 {"wildcard and more, PoSt", "PoSt", "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5201 {"wildcard and more, PUT", http.MethodPut, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5202 {"wildcard and more, PATCH", http.MethodPatch, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5203 {"wildcard and more, DELETE", http.MethodDelete, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5204 {"wildcard and more, OPTIONS", http.MethodOptions, "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5205 {"wildcard and more, CONNECT", http.MethodConnect, "/api1/wildcardAndMore", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5206 {"wildcard and more, TRACE", http.MethodTrace, "/api1/wildcardAndMore", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5207 {"wildcard and more, bReW", "bReW", "/api1/wildcardAndMore", http.Header{}, http.StatusOK, ""}, 5208 5209 {"blocked, GET", http.MethodGet, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5210 {"blocked, HEAD", http.MethodHead, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5211 {"blocked, POST", http.MethodPost, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5212 {"blocked, PUT", http.MethodPut, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5213 {"blocked, PATCH", http.MethodPatch, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5214 {"blocked, DELETE", http.MethodDelete, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5215 {"blocked, OPTIONS", http.MethodOptions, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5216 {"blocked, CONNECT", http.MethodConnect, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5217 {"blocked, TRACE", http.MethodTrace, "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5218 {"blocked, BREW", "BREW", "/api1/blocked", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5219 5220 {"restricted methods override, GET", http.MethodGet, "/api2/restricted", http.Header{}, http.StatusOK, ""}, 5221 {"restricted methods override, HEAD", http.MethodHead, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5222 {"restricted methods override, POST", http.MethodPost, "/api2/restricted", http.Header{}, http.StatusOK, ""}, 5223 {"restricted methods override, PUT", http.MethodPut, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5224 {"restricted methods override, PATCH", http.MethodPatch, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5225 {"restricted methods override, DELETE", http.MethodDelete, "/api2/restricted", http.Header{}, http.StatusOK, ""}, 5226 {"restricted methods override, OPTIONS", http.MethodOptions, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5227 {"restricted methods override, CONNECT", http.MethodConnect, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5228 {"restricted methods override, TRACE", http.MethodTrace, "/api2/restricted", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5229 {"restricted methods override, BREW", "BREW", "/api2/restricted", http.Header{}, http.StatusOK, ""}, 5230 5231 {"restricted by api only, GET", http.MethodGet, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5232 {"restricted by api only, HEAD", http.MethodHead, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5233 {"restricted by api only, POST", http.MethodPost, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5234 {"restricted by api only, PUT", http.MethodPut, "/api2/restrictedByApiOnly", http.Header{}, http.StatusOK, ""}, 5235 {"restricted by api only, PATCH", http.MethodPatch, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5236 {"restricted by api only, DELETE", http.MethodDelete, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5237 {"restricted by api only, OPTIONS", http.MethodOptions, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5238 {"restricted by api only, CONNECT", http.MethodConnect, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5239 {"restricted by api only, TRACE", http.MethodTrace, "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5240 {"restricted by api only, BREW", "BREW", "/api2/restrictedByApiOnly", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5241 5242 {"files, GET", http.MethodGet, "/index.html", http.Header{}, http.StatusOK, ""}, 5243 {"files, HEAD", http.MethodHead, "/index.html", http.Header{}, http.StatusOK, ""}, 5244 {"files, POST", http.MethodPost, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5245 {"files, PUT", http.MethodPut, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5246 {"files, PATCH", http.MethodPatch, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5247 {"files, DELETE", http.MethodDelete, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5248 {"files, OPTIONS", http.MethodOptions, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5249 {"files, CONNECT", http.MethodConnect, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5250 {"files, TRACE", http.MethodTrace, "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5251 {"files, BREW", "BREW", "/index.html", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5252 {"files, CORS preflight", http.MethodOptions, "/index.html", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""}, 5253 5254 {"spa, GET", http.MethodGet, "/app/foo", http.Header{}, http.StatusOK, ""}, 5255 {"spa, HEAD", http.MethodHead, "/app/foo", http.Header{}, http.StatusOK, ""}, 5256 {"spa, POST", http.MethodPost, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5257 {"spa, PUT", http.MethodPut, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5258 {"spa, PATCH", http.MethodPatch, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5259 {"spa, DELETE", http.MethodDelete, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5260 {"spa, OPTIONS", http.MethodOptions, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5261 {"spa, CONNECT", http.MethodConnect, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5262 {"spa, TRACE", http.MethodTrace, "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5263 {"spa, BREW", "BREW", "/app/foo", http.Header{}, http.StatusMethodNotAllowed, "method not allowed error"}, 5264 {"spa, CORS preflight", http.MethodOptions, "/app/foo", http.Header{"Origin": []string{"https://www.example.com"}, "Access-Control-Request-Method": []string{"POST"}, "Access-Control-Request-Headers": []string{"Authorization"}}, http.StatusNoContent, ""}, 5265 } { 5266 t.Run(tc.name, func(subT *testing.T) { 5267 helper := test.New(subT) 5268 logHook.Reset() 5269 req, err := http.NewRequest(tc.method, "http://example.com:8080"+tc.path, nil) 5270 helper.Must(err) 5271 req.Header = tc.requestHeaders 5272 5273 res, err := client.Do(req) 5274 helper.Must(err) 5275 5276 if tc.status != res.StatusCode { 5277 subT.Errorf("Unexpected status code given; want: %d; got: %d", tc.status, res.StatusCode) 5278 } 5279 5280 couperError := res.Header.Get("Couper-Error") 5281 if tc.couperError != couperError { 5282 subT.Errorf("Unexpected couper-error given; want: %q; got: %q", tc.couperError, couperError) 5283 } 5284 }) 5285 } 5286 } 5287 5288 func TestAllowedMethodsCORS_Preflight(t *testing.T) { 5289 client := newClient() 5290 5291 confPath := "testdata/integration/config/11_couper.hcl" 5292 shutdown, logHook := newCouper(confPath, test.New(t)) 5293 defer shutdown() 5294 5295 type testCase struct { 5296 name string 5297 path string 5298 requestMethod string 5299 status int 5300 allowMethods []string 5301 couperError string 5302 } 5303 5304 for _, tc := range []testCase{ 5305 {"unrestricted, CORS preflight, POST allowed", "/api1/unrestricted", http.MethodPost, http.StatusNoContent, []string{"POST"}, ""}, 5306 {"restricted, CORS preflight, POST allowed", "/api1/restricted", http.MethodPost, http.StatusNoContent, []string{"POST"}, ""}, // CORS preflight ok even if OPTIONS is otherwise not allowed 5307 {"restricted, CORS preflight, PUT not allowed", "/api1/restricted", http.MethodPut, http.StatusNoContent, nil, ""}, 5308 } { 5309 t.Run(tc.name, func(subT *testing.T) { 5310 helper := test.New(subT) 5311 logHook.Reset() 5312 req, err := http.NewRequest(http.MethodOptions, "http://example.com:8080"+tc.path, nil) 5313 helper.Must(err) 5314 req.Header.Set("Origin", "https://www.example.com") 5315 req.Header.Set("Access-Control-Request-Method", tc.requestMethod) 5316 5317 res, err := client.Do(req) 5318 helper.Must(err) 5319 5320 if tc.status != res.StatusCode { 5321 subT.Errorf("Unexpected status code given; want: %d; got: %d", tc.status, res.StatusCode) 5322 } 5323 5324 allowMethods := res.Header.Values("Access-Control-Allow-Methods") 5325 if !cmp.Equal(tc.allowMethods, allowMethods) { 5326 subT.Errorf(cmp.Diff(tc.allowMethods, allowMethods)) 5327 } 5328 5329 couperError := res.Header.Get("Couper-Error") 5330 if tc.couperError != couperError { 5331 subT.Errorf("Unexpected couper-error given; want: %q; got: %q", tc.couperError, couperError) 5332 } 5333 }) 5334 } 5335 } 5336 5337 func TestEndpoint_ResponseNilEvaluation(t *testing.T) { 5338 client := newClient() 5339 5340 shutdown, hook := newCouper("testdata/integration/endpoint_eval/20_couper.hcl", test.New(t)) 5341 defer shutdown() 5342 5343 type testCase struct { 5344 path string 5345 expVal bool 5346 expCtyVal string 5347 } 5348 5349 for _, tc := range []testCase{ 5350 {"/1stchild", true, ""}, 5351 {"/2ndchild/no", false, ""}, 5352 {"/child-chain/no", false, ""}, 5353 {"/list-idx", true, ""}, 5354 {"/list-idx-splat", true, ""}, 5355 {"/list-idx/no", false, ""}, 5356 {"/list-idx-chain/no", false, ""}, 5357 {"/list-idx-key-chain/no", false, ""}, 5358 {"/root/no", false, ""}, 5359 {"/tpl", true, ""}, 5360 {"/for", true, ""}, 5361 {"/conditional/false", true, ""}, 5362 {"/conditional/true", false, ""}, 5363 {"/conditional/nested", true, ""}, 5364 {"/conditional/nested/true", true, ""}, 5365 {"/conditional/nested/false", true, ""}, 5366 {"/functions/arg-items", true, `{"foo":"bar","obj":{"key":"val"},"xxxx":null}`}, 5367 {"/functions/tuple-expr", true, `{"array":["a","b"]}`}, 5368 {"/rte1", true, "2"}, 5369 {"/rte2", true, "2"}, 5370 {"/ie1", true, "2"}, 5371 {"/ie2", true, "2"}, 5372 {"/uoe1", true, "-2"}, 5373 {"/uoe2", true, "true"}, 5374 {"/bad/dereference/string?foo=bar", false, ""}, 5375 {"/bad/dereference/array?foo=bar", false, ""}, 5376 } { 5377 t.Run(tc.path[1:], func(subT *testing.T) { 5378 helper := test.New(subT) 5379 5380 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 5381 helper.Must(err) 5382 5383 hook.Reset() 5384 defer func() { 5385 if subT.Failed() { 5386 time.Sleep(time.Millisecond * 100) 5387 for _, entry := range hook.AllEntries() { 5388 s, _ := entry.String() 5389 println(s) 5390 } 5391 } 5392 }() 5393 5394 res, err := client.Do(req) 5395 helper.Must(err) 5396 5397 if res.StatusCode != http.StatusOK { 5398 subT.Errorf("Expected Status OK, got: %d", res.StatusCode) 5399 return 5400 } 5401 5402 defer func() { 5403 if subT.Failed() { 5404 for k := range res.Header { 5405 subT.Logf("%s: %s", k, res.Header.Get(k)) 5406 } 5407 } 5408 }() 5409 5410 val, ok := res.Header[http.CanonicalHeaderKey("X-Value")] 5411 if !tc.expVal && ok { 5412 subT.Errorf("%q: expected no value, got: %q", tc.path, val) 5413 } else if tc.expVal && !ok { 5414 subT.Errorf("%q: expected X-Value header, got: nothing", tc.path) 5415 } 5416 5417 if res.Header.Get("Z-Value") != "y" { 5418 subT.Errorf("additional header Z-Value should always been written") 5419 } 5420 5421 if tc.expCtyVal != "" && tc.expCtyVal != val[0] { 5422 subT.Errorf("Want: %s, got: %v", tc.expCtyVal, val[0]) 5423 } 5424 5425 }) 5426 } 5427 } 5428 5429 func TestEndpoint_ConditionalEvaluationError(t *testing.T) { 5430 client := newClient() 5431 5432 wd, werr := os.Getwd() 5433 if werr != nil { 5434 t.Fatal(werr) 5435 } 5436 wd = wd + "/testdata/integration/endpoint_eval" 5437 5438 shutdown, hook := newCouper("testdata/integration/endpoint_eval/20_couper.hcl", test.New(t)) 5439 defer shutdown() 5440 5441 type testCase struct { 5442 path string 5443 expMessage string 5444 } 5445 5446 for _, tc := range []testCase{ 5447 {"/conditional/null", wd + "/20_couper.hcl:281,16-20: Null condition; The condition value is null. Conditions must either be true or false."}, 5448 {"/conditional/string", wd + "/20_couper.hcl:287,16-21: Incorrect condition type; The condition expression must be of type bool."}, 5449 {"/conditional/number", wd + "/20_couper.hcl:293,16-17: Incorrect condition type; The condition expression must be of type bool."}, 5450 {"/conditional/tuple", wd + "/20_couper.hcl:299,16-18: Incorrect condition type; The condition expression must be of type bool."}, 5451 {"/conditional/object", wd + "/20_couper.hcl:305,16-18: Incorrect condition type; The condition expression must be of type bool."}, 5452 {"/conditional/string/expr", wd + "/20_couper.hcl:311,16-30: Incorrect condition type; The condition expression must be of type bool."}, 5453 {"/conditional/number/expr", wd + "/20_couper.hcl:317,16-26: Incorrect condition type; The condition expression must be of type bool."}, 5454 } { 5455 t.Run(tc.path[1:], func(subT *testing.T) { 5456 helper := test.New(subT) 5457 5458 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 5459 helper.Must(err) 5460 5461 hook.Reset() 5462 defer func() { 5463 if subT.Failed() { 5464 time.Sleep(time.Millisecond * 100) 5465 for _, entry := range hook.AllEntries() { 5466 s, _ := entry.String() 5467 println(s) 5468 } 5469 } 5470 }() 5471 5472 res, err := client.Do(req) 5473 helper.Must(err) 5474 5475 if res.StatusCode != http.StatusInternalServerError { 5476 subT.Errorf("Expected Status InternalServerError, got: %d", res.StatusCode) 5477 return 5478 } 5479 5480 time.Sleep(time.Millisecond * 100) 5481 entry := hook.LastEntry() 5482 if entry != nil && entry.Level == logrus.ErrorLevel { 5483 if entry.Message != tc.expMessage { 5484 subT.Errorf("wrong error message,\nexp: %s\ngot: %s", tc.expMessage, entry.Message) 5485 } 5486 } 5487 }) 5488 } 5489 } 5490 5491 func TestEndpoint_ForLoop(t *testing.T) { 5492 client := newClient() 5493 5494 shutdown, hook := newCouper("testdata/integration/endpoint_eval/21_couper.hcl", test.New(t)) 5495 defer shutdown() 5496 5497 type testCase struct { 5498 path string 5499 header http.Header 5500 expResult string 5501 } 5502 5503 for _, tc := range []testCase{ 5504 {"/for0", http.Header{}, `["a","b"]`}, 5505 {"/for1", http.Header{}, `[0,1]`}, 5506 {"/for2", http.Header{}, `{"a":0,"b":1}`}, 5507 {"/for3", http.Header{}, `{"a":[0,1],"b":[2]}`}, 5508 {"/for4", http.Header{}, `["a","b"]`}, 5509 {"/for5", http.Header{"x-1": []string{"val1"}, "x-2": []string{"val2"}, "y": []string{`["x-1","x-2"]`}, "z": []string{"pfx"}}, `{"pfx-x-1":"val1","pfx-x-2":"val2"}`}, 5510 } { 5511 t.Run(tc.path[1:], func(subT *testing.T) { 5512 helper := test.New(subT) 5513 5514 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 5515 req.Header = tc.header 5516 helper.Must(err) 5517 5518 hook.Reset() 5519 5520 res, err := client.Do(req) 5521 helper.Must(err) 5522 5523 resBytes, err := io.ReadAll(res.Body) 5524 helper.Must(err) 5525 5526 helper.Must(res.Body.Close()) 5527 5528 if res.StatusCode != http.StatusOK { 5529 subT.Errorf("Expected Status OK, got: %d", res.StatusCode) 5530 return 5531 } 5532 5533 result := string(resBytes) 5534 if result != tc.expResult { 5535 subT.Errorf("Want: %s, got: %v", tc.expResult, result) 5536 } 5537 }) 5538 } 5539 } 5540 5541 func TestWildcardURLAttribute(t *testing.T) { 5542 client := newClient() 5543 5544 shutdown, hook := newCouper("testdata/integration/url/07_couper.hcl", test.New(t)) 5545 defer shutdown() 5546 5547 for _, testcase := range []struct{ path, expectedPath, expectedQuery string }{ 5548 {"/req/anything", "/anything", ""}, 5549 {"/req/anything/", "/anything/", ""}, 5550 {"/req-query/anything/?a=b", "/anything/", "a=c"}, 5551 {"/req-backend/anything/?a=b", "/anything/", "a=c"}, 5552 {"/proxy/anything", "/anything", ""}, 5553 {"/proxy/anything/", "/anything/", ""}, 5554 {"/proxy-query/anything/?a=b", "/anything/", "a=c"}, 5555 {"/proxy-backend/anything", "/anything", ""}, 5556 {"/proxy-backend-rel/anything?a=b", "/anything", "a=c"}, 5557 {"/proxy-backend-path/other-wildcard?a=b", "/anything", "a=c"}, 5558 } { 5559 t.Run(testcase.path[1:], func(st *testing.T) { 5560 helper := test.New(st) 5561 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+testcase.path, nil) 5562 helper.Must(err) 5563 5564 hook.Reset() 5565 5566 res, err := client.Do(req) 5567 helper.Must(err) 5568 5569 if res.StatusCode != http.StatusOK { 5570 st.Error("expected status OK") 5571 } 5572 5573 b, err := io.ReadAll(res.Body) 5574 helper.Must(res.Body.Close()) 5575 helper.Must(err) 5576 5577 type result struct { 5578 Path string 5579 RawQuery string 5580 } 5581 r := result{} 5582 helper.Must(json.Unmarshal(b, &r)) 5583 //st.Logf("%v", r) 5584 5585 if testcase.expectedPath != r.Path { 5586 st.Errorf("Expected path: %q, got: %q", testcase.expectedPath, r.Path) 5587 } 5588 5589 if testcase.expectedQuery != r.RawQuery { 5590 st.Errorf("Expected query: %q, got: %q", testcase.expectedQuery, r.RawQuery) 5591 } 5592 }) 5593 } 5594 } 5595 5596 func TestEnvironmentSetting(t *testing.T) { 5597 helper := test.New(t) 5598 tests := []struct { 5599 env string 5600 }{ 5601 {"foo"}, 5602 {"bar"}, 5603 } 5604 5605 template := ` 5606 server { 5607 endpoint "/" { 5608 response { 5609 environment "foo" { 5610 headers = { X-Env: "foo" } 5611 } 5612 environment "bar" { 5613 headers = { X-Env: "bar" } 5614 } 5615 } 5616 } 5617 } 5618 settings { 5619 environment = "%s" 5620 } 5621 ` 5622 5623 file, err := os.CreateTemp("", "tmpfile-") 5624 helper.Must(err) 5625 defer file.Close() 5626 defer os.Remove(file.Name()) 5627 5628 client := newClient() 5629 for _, tt := range tests { 5630 t.Run(tt.env, func(subT *testing.T) { 5631 config := []byte(fmt.Sprintf(template, tt.env)) 5632 err := os.Truncate(file.Name(), 0) 5633 helper.Must(err) 5634 _, err = file.Seek(0, 0) 5635 helper.Must(err) 5636 _, err = file.Write(config) 5637 helper.Must(err) 5638 5639 couperConfig, err := configload.LoadFile(file.Name(), "") 5640 helper.Must(err) 5641 5642 shutdown, _ := newCouperWithConfig(couperConfig, helper) 5643 defer shutdown() 5644 5645 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/", nil) 5646 helper.Must(err) 5647 5648 res, err := client.Do(req) 5649 helper.Must(err) 5650 5651 if header := res.Header.Get("X-Env"); header != tt.env { 5652 subT.Errorf("Unexpected header:\n\tWant: %q\n\tGot: %q", tt.env, header) 5653 } 5654 }) 5655 } 5656 } 5657 5658 func TestWildcardVsEmptyPathParams(t *testing.T) { 5659 client := newClient() 5660 5661 shutdown, _ := newCouper("testdata/integration/url/08_couper.hcl", test.New(t)) 5662 defer shutdown() 5663 5664 type testCase struct { 5665 path string 5666 expected string 5667 } 5668 5669 for _, tc := range []testCase{ 5670 {"/foo", "/**"}, 5671 {"/p1/A/B/C", "/**"}, 5672 {"/p1/A/B/", "/p1/{x}/{y}"}, 5673 {"/p1/A//", "/**"}, 5674 {"/p1///", "/**"}, 5675 {"/p1//", "/**"}, 5676 {"/p1/A/B", "/p1/{x}/{y}"}, 5677 {"/p1/A/", "/**"}, 5678 {"/p1/A", "/**"}, 5679 {"/p1/", "/**"}, 5680 {"/p1", "/**"}, 5681 {"/p2/A/B/C", "/p2/**"}, 5682 {"/p2/A/B/", "/p2/{x}/{y}"}, 5683 {"/p2/A/B", "/p2/{x}/{y}"}, 5684 {"/p2/A/", "/p2/**"}, 5685 {"/p2/A", "/p2/**"}, 5686 {"/p2/", "/p2/**"}, 5687 {"/p2", "/p2/**"}, 5688 {"/p3/A/B/C", "/p3/**"}, 5689 {"/p3/A/B/", "/p3/{x}/{y}"}, 5690 {"/p3/A/B", "/p3/{x}/{y}"}, 5691 {"/p3/A/", "/p3/{x}"}, 5692 {"/p3/A", "/p3/{x}"}, 5693 {"/p3/", "/p3/**"}, 5694 {"/p3", "/p3/**"}, 5695 } { 5696 t.Run(tc.path, func(subT *testing.T) { 5697 helper := test.New(subT) 5698 req, err := http.NewRequest(http.MethodGet, "http://localhost:8080"+tc.path, nil) 5699 helper.Must(err) 5700 5701 res, err := client.Do(req) 5702 helper.Must(err) 5703 5704 if res.StatusCode != http.StatusOK { 5705 subT.Errorf("Unexpected status: want: 200, got %d", res.StatusCode) 5706 } 5707 5708 match := res.Header.Get("Match") 5709 if match != tc.expected { 5710 subT.Errorf("Unexpected match for %s: want %s, got %s", tc.path, tc.expected, match) 5711 } 5712 }) 5713 } 5714 }