k8s.io/apimachinery@v0.29.2/pkg/util/net/http_test.go (about) 1 //go:build go1.8 2 // +build go1.8 3 4 /* 5 Copyright 2016 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 19 20 package net 21 22 import ( 23 "crypto/tls" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "net/url" 29 "reflect" 30 "strings" 31 "testing" 32 33 "github.com/stretchr/testify/assert" 34 netutils "k8s.io/utils/net" 35 ) 36 37 func TestGetClientIP(t *testing.T) { 38 ipString := "10.0.0.1" 39 ip := netutils.ParseIPSloppy(ipString) 40 invalidIPString := "invalidIPString" 41 testCases := []struct { 42 Request http.Request 43 ExpectedIP net.IP 44 }{ 45 { 46 Request: http.Request{}, 47 }, 48 { 49 Request: http.Request{ 50 Header: map[string][]string{ 51 "X-Real-Ip": {ipString}, 52 }, 53 }, 54 ExpectedIP: ip, 55 }, 56 { 57 Request: http.Request{ 58 Header: map[string][]string{ 59 "X-Real-Ip": {invalidIPString}, 60 }, 61 }, 62 }, 63 { 64 Request: http.Request{ 65 Header: map[string][]string{ 66 "X-Forwarded-For": {ipString}, 67 }, 68 }, 69 ExpectedIP: ip, 70 }, 71 { 72 Request: http.Request{ 73 Header: map[string][]string{ 74 "X-Forwarded-For": {invalidIPString}, 75 }, 76 }, 77 }, 78 { 79 Request: http.Request{ 80 Header: map[string][]string{ 81 "X-Forwarded-For": {invalidIPString + "," + ipString}, 82 }, 83 }, 84 ExpectedIP: ip, 85 }, 86 { 87 Request: http.Request{ 88 // RemoteAddr is in the form host:port 89 RemoteAddr: ipString + ":1234", 90 }, 91 ExpectedIP: ip, 92 }, 93 { 94 Request: http.Request{ 95 RemoteAddr: invalidIPString, 96 }, 97 }, 98 { 99 Request: http.Request{ 100 Header: map[string][]string{ 101 "X-Forwarded-For": {invalidIPString}, 102 }, 103 // RemoteAddr is in the form host:port 104 RemoteAddr: ipString, 105 }, 106 ExpectedIP: ip, 107 }, 108 } 109 110 for i, test := range testCases { 111 if a, e := GetClientIP(&test.Request), test.ExpectedIP; reflect.DeepEqual(e, a) != true { 112 t.Fatalf("test case %d failed. expected: %v, actual: %v", i, e, a) 113 } 114 } 115 } 116 117 func TestAppendForwardedForHeader(t *testing.T) { 118 testCases := []struct { 119 addr, forwarded, expected string 120 }{ 121 {"1.2.3.4:8000", "", "1.2.3.4"}, 122 {"1.2.3.4:8000", "8.8.8.8", "8.8.8.8, 1.2.3.4"}, 123 {"1.2.3.4:8000", "8.8.8.8, 1.2.3.4", "8.8.8.8, 1.2.3.4, 1.2.3.4"}, 124 {"1.2.3.4:8000", "foo,bar", "foo,bar, 1.2.3.4"}, 125 } 126 for i, test := range testCases { 127 req := &http.Request{ 128 RemoteAddr: test.addr, 129 Header: make(http.Header), 130 } 131 if test.forwarded != "" { 132 req.Header.Set("X-Forwarded-For", test.forwarded) 133 } 134 135 AppendForwardedForHeader(req) 136 actual := req.Header.Get("X-Forwarded-For") 137 if actual != test.expected { 138 t.Errorf("[%d] Expected %q, Got %q", i, test.expected, actual) 139 } 140 } 141 } 142 143 func TestProxierWithNoProxyCIDR(t *testing.T) { 144 testCases := []struct { 145 name string 146 noProxy string 147 url string 148 149 expectedDelegated bool 150 }{ 151 { 152 name: "no env", 153 url: "https://192.168.143.1/api", 154 expectedDelegated: true, 155 }, 156 { 157 name: "no cidr", 158 noProxy: "192.168.63.1", 159 url: "https://192.168.143.1/api", 160 expectedDelegated: true, 161 }, 162 { 163 name: "hostname", 164 noProxy: "192.168.63.0/24,192.168.143.0/24", 165 url: "https://my-hostname/api", 166 expectedDelegated: true, 167 }, 168 { 169 name: "match second cidr", 170 noProxy: "192.168.63.0/24,192.168.143.0/24", 171 url: "https://192.168.143.1/api", 172 expectedDelegated: false, 173 }, 174 { 175 name: "match second cidr with host:port", 176 noProxy: "192.168.63.0/24,192.168.143.0/24", 177 url: "https://192.168.143.1:8443/api", 178 expectedDelegated: false, 179 }, 180 { 181 name: "IPv6 cidr", 182 noProxy: "2001:db8::/48", 183 url: "https://[2001:db8::1]/api", 184 expectedDelegated: false, 185 }, 186 { 187 name: "IPv6+port cidr", 188 noProxy: "2001:db8::/48", 189 url: "https://[2001:db8::1]:8443/api", 190 expectedDelegated: false, 191 }, 192 { 193 name: "IPv6, not matching cidr", 194 noProxy: "2001:db8::/48", 195 url: "https://[2001:db8:1::1]/api", 196 expectedDelegated: true, 197 }, 198 { 199 name: "IPv6+port, not matching cidr", 200 noProxy: "2001:db8::/48", 201 url: "https://[2001:db8:1::1]:8443/api", 202 expectedDelegated: true, 203 }, 204 } 205 206 for _, test := range testCases { 207 t.Setenv("NO_PROXY", test.noProxy) 208 actualDelegated := false 209 proxyFunc := NewProxierWithNoProxyCIDR(func(req *http.Request) (*url.URL, error) { 210 actualDelegated = true 211 return nil, nil 212 }) 213 214 req, err := http.NewRequest("GET", test.url, nil) 215 if err != nil { 216 t.Errorf("%s: unexpected err: %v", test.name, err) 217 continue 218 } 219 if _, err := proxyFunc(req); err != nil { 220 t.Errorf("%s: unexpected err: %v", test.name, err) 221 continue 222 } 223 224 if test.expectedDelegated != actualDelegated { 225 t.Errorf("%s: expected %v, got %v", test.name, test.expectedDelegated, actualDelegated) 226 continue 227 } 228 } 229 } 230 231 type fakeTLSClientConfigHolder struct { 232 called bool 233 } 234 235 func (f *fakeTLSClientConfigHolder) TLSClientConfig() *tls.Config { 236 f.called = true 237 return nil 238 } 239 func (f *fakeTLSClientConfigHolder) RoundTrip(*http.Request) (*http.Response, error) { 240 return nil, nil 241 } 242 243 func TestTLSClientConfigHolder(t *testing.T) { 244 rt := &fakeTLSClientConfigHolder{} 245 TLSClientConfig(rt) 246 247 if !rt.called { 248 t.Errorf("didn't find tls config") 249 } 250 } 251 252 func TestJoinPreservingTrailingSlash(t *testing.T) { 253 tests := []struct { 254 a string 255 b string 256 want string 257 }{ 258 // All empty 259 {"", "", ""}, 260 261 // Empty a 262 {"", "/", "/"}, 263 {"", "foo", "foo"}, 264 {"", "/foo", "/foo"}, 265 {"", "/foo/", "/foo/"}, 266 267 // Empty b 268 {"/", "", "/"}, 269 {"foo", "", "foo"}, 270 {"/foo", "", "/foo"}, 271 {"/foo/", "", "/foo/"}, 272 273 // Both populated 274 {"/", "/", "/"}, 275 {"foo", "foo", "foo/foo"}, 276 {"/foo", "/foo", "/foo/foo"}, 277 {"/foo/", "/foo/", "/foo/foo/"}, 278 } 279 for _, tt := range tests { 280 name := fmt.Sprintf("%q+%q=%q", tt.a, tt.b, tt.want) 281 t.Run(name, func(t *testing.T) { 282 if got := JoinPreservingTrailingSlash(tt.a, tt.b); got != tt.want { 283 t.Errorf("JoinPreservingTrailingSlash() = %v, want %v", got, tt.want) 284 } 285 }) 286 } 287 } 288 289 func TestAllowsHTTP2(t *testing.T) { 290 testcases := []struct { 291 Name string 292 Transport *http.Transport 293 ExpectAllows bool 294 }{ 295 { 296 Name: "empty", 297 Transport: &http.Transport{}, 298 ExpectAllows: true, 299 }, 300 { 301 Name: "empty tlsconfig", 302 Transport: &http.Transport{TLSClientConfig: &tls.Config{}}, 303 ExpectAllows: true, 304 }, 305 { 306 Name: "zero-length NextProtos", 307 Transport: &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{}}}, 308 ExpectAllows: true, 309 }, 310 { 311 Name: "includes h2 in NextProtos after", 312 Transport: &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"http/1.1", "h2"}}}, 313 ExpectAllows: true, 314 }, 315 { 316 Name: "includes h2 in NextProtos before", 317 Transport: &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"h2", "http/1.1"}}}, 318 ExpectAllows: true, 319 }, 320 { 321 Name: "includes h2 in NextProtos between", 322 Transport: &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"http/1.1", "h2", "h3"}}}, 323 ExpectAllows: true, 324 }, 325 { 326 Name: "excludes h2 in NextProtos", 327 Transport: &http.Transport{TLSClientConfig: &tls.Config{NextProtos: []string{"http/1.1"}}}, 328 ExpectAllows: false, 329 }, 330 } 331 332 for _, tc := range testcases { 333 t.Run(tc.Name, func(t *testing.T) { 334 allows := allowsHTTP2(tc.Transport) 335 if allows != tc.ExpectAllows { 336 t.Errorf("expected %v, got %v", tc.ExpectAllows, allows) 337 } 338 }) 339 } 340 } 341 342 func TestSourceIPs(t *testing.T) { 343 tests := []struct { 344 name string 345 realIP string 346 forwardedFor string 347 remoteAddr string 348 expected []string 349 }{{ 350 name: "no headers, missing remoteAddr", 351 expected: []string{}, 352 }, { 353 name: "no headers, just remoteAddr host:port", 354 remoteAddr: "1.2.3.4:555", 355 expected: []string{"1.2.3.4"}, 356 }, { 357 name: "no headers, just remoteAddr host", 358 remoteAddr: "1.2.3.4", 359 expected: []string{"1.2.3.4"}, 360 }, { 361 name: "empty forwarded-for chain", 362 forwardedFor: " ", 363 remoteAddr: "1.2.3.4", 364 expected: []string{"1.2.3.4"}, 365 }, { 366 name: "invalid forwarded-for chain", 367 forwardedFor: "garbage garbage values!", 368 remoteAddr: "1.2.3.4", 369 expected: []string{"1.2.3.4"}, 370 }, { 371 name: "partially invalid forwarded-for chain", 372 forwardedFor: "garbage garbage values!,4.5.6.7", 373 remoteAddr: "1.2.3.4", 374 expected: []string{"4.5.6.7", "1.2.3.4"}, 375 }, { 376 name: "valid forwarded-for chain", 377 forwardedFor: "120.120.120.126,2.2.2.2,4.5.6.7", 378 remoteAddr: "1.2.3.4", 379 expected: []string{"120.120.120.126", "2.2.2.2", "4.5.6.7", "1.2.3.4"}, 380 }, { 381 name: "valid forwarded-for chain with redundant remoteAddr", 382 forwardedFor: "2.2.2.2,1.2.3.4", 383 remoteAddr: "1.2.3.4", 384 expected: []string{"2.2.2.2", "1.2.3.4"}, 385 }, { 386 name: "invalid Real-Ip", 387 realIP: "garbage, just garbage!", 388 remoteAddr: "1.2.3.4", 389 expected: []string{"1.2.3.4"}, 390 }, { 391 name: "invalid Real-Ip with forwarded-for", 392 realIP: "garbage, just garbage!", 393 forwardedFor: "2.2.2.2", 394 remoteAddr: "1.2.3.4", 395 expected: []string{"2.2.2.2", "1.2.3.4"}, 396 }, { 397 name: "valid Real-Ip", 398 realIP: "2.2.2.2", 399 remoteAddr: "1.2.3.4", 400 expected: []string{"2.2.2.2", "1.2.3.4"}, 401 }, { 402 name: "redundant Real-Ip", 403 realIP: "1.2.3.4", 404 remoteAddr: "1.2.3.4", 405 expected: []string{"1.2.3.4"}, 406 }, { 407 name: "valid Real-Ip with forwarded-for", 408 realIP: "2.2.2.2", 409 forwardedFor: "120.120.120.126,4.5.6.7", 410 remoteAddr: "1.2.3.4", 411 expected: []string{"120.120.120.126", "4.5.6.7", "2.2.2.2", "1.2.3.4"}, 412 }, { 413 name: "redundant Real-Ip with forwarded-for", 414 realIP: "2.2.2.2", 415 forwardedFor: "120.120.120.126,2.2.2.2,4.5.6.7", 416 remoteAddr: "1.2.3.4", 417 expected: []string{"120.120.120.126", "2.2.2.2", "4.5.6.7", "1.2.3.4"}, 418 }, { 419 name: "full redundancy", 420 realIP: "1.2.3.4", 421 forwardedFor: "1.2.3.4", 422 remoteAddr: "1.2.3.4", 423 expected: []string{"1.2.3.4"}, 424 }, { 425 name: "full ipv6", 426 realIP: "abcd:ef01:2345:6789:abcd:ef01:2345:6789", 427 forwardedFor: "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111,0:1111:2222:3333:4444:5555:6666:7777", 428 remoteAddr: "aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa", 429 expected: []string{ 430 "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111", 431 "0:1111:2222:3333:4444:5555:6666:7777", 432 "abcd:ef01:2345:6789:abcd:ef01:2345:6789", 433 "aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa", 434 }, 435 }, { 436 name: "mixed ipv4 ipv6", 437 forwardedFor: "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111,1.2.3.4", 438 remoteAddr: "0:0:0:0:0:ffff:102:304", // ipv6 equivalent to 1.2.3.4 439 expected: []string{ 440 "aaaa:bbbb:cccc:dddd:eeee:ffff:0:1111", 441 "1.2.3.4", 442 }, 443 }} 444 445 for _, test := range tests { 446 t.Run(test.name, func(t *testing.T) { 447 req, _ := http.NewRequest("GET", "https://cluster.k8s.io/apis/foobars/v1/foo/bar", nil) 448 req.RemoteAddr = test.remoteAddr 449 if test.forwardedFor != "" { 450 req.Header.Set("X-Forwarded-For", test.forwardedFor) 451 } 452 if test.realIP != "" { 453 req.Header.Set("X-Real-Ip", test.realIP) 454 } 455 456 actualIPs := SourceIPs(req) 457 actual := make([]string, len(actualIPs)) 458 for i, ip := range actualIPs { 459 actual[i] = ip.String() 460 } 461 462 assert.Equal(t, test.expected, actual) 463 }) 464 } 465 } 466 467 func TestParseWarningHeader(t *testing.T) { 468 tests := []struct { 469 name string 470 471 header string 472 473 wantResult WarningHeader 474 wantRemainder string 475 wantErr string 476 }{ 477 // invalid cases 478 { 479 name: "empty", 480 header: ``, 481 wantErr: "fewer than 3 segments", 482 }, 483 { 484 name: "bad code", 485 header: `A B`, 486 wantErr: "fewer than 3 segments", 487 }, 488 { 489 name: "short code", 490 header: `1 - "text"`, 491 wantErr: "not 3 digits", 492 }, 493 { 494 name: "bad code", 495 header: `A - "text"`, 496 wantErr: "not 3 digits", 497 }, 498 { 499 name: "invalid date quoting", 500 header: ` 299 - "text\"\\\a\b\c" "Tue, 15 Nov 1994 08:12:31 GMT `, 501 wantErr: "unterminated date segment", 502 }, 503 { 504 name: "invalid post-date", 505 header: ` 299 - "text\"\\\a\b\c" "Tue, 15 Nov 1994 08:12:31 GMT" other`, 506 wantErr: "unexpected token after warn-date", 507 }, 508 { 509 name: "agent control character", 510 header: " 299 agent\u0000name \"text\"", 511 wantErr: "invalid agent", 512 }, 513 { 514 name: "agent non-utf8 character", 515 header: " 299 agent\xc5name \"text\"", 516 wantErr: "invalid agent", 517 }, 518 { 519 name: "text control character", 520 header: " 299 - \"text\u0000\"content", 521 wantErr: "invalid text", 522 }, 523 { 524 name: "text non-utf8 character", 525 header: " 299 - \"text\xc5\"content", 526 wantErr: "invalid text", 527 }, 528 529 // valid cases 530 { 531 name: "ok", 532 header: `299 - "text"`, 533 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text`}, 534 }, 535 { 536 name: "ok", 537 header: `299 - "text\"\\\a\b\c"`, 538 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 539 }, 540 // big code 541 { 542 name: "big code", 543 header: `321 - "text"`, 544 wantResult: WarningHeader{Code: 321, Agent: "-", Text: "text"}, 545 }, 546 // RFC 2047 decoding 547 { 548 name: "ok, rfc 2047, iso-8859-1, q", 549 header: `299 - "=?iso-8859-1?q?this=20is=20some=20text?="`, 550 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `this is some text`}, 551 }, 552 { 553 name: "ok, rfc 2047, utf-8, b", 554 header: `299 - "=?UTF-8?B?VGhpcyBpcyBhIGhvcnNleTog8J+Qjg==?= And =?UTF-8?B?VGhpcyBpcyBhIGhvcnNleTog8J+Qjg==?="`, 555 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `This is a horsey: 🐎 And This is a horsey: 🐎`}, 556 }, 557 { 558 name: "ok, rfc 2047, utf-8, q", 559 header: `299 - "=?UTF-8?Q?This is a \"horsey\": =F0=9F=90=8E?="`, 560 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `This is a "horsey": 🐎`}, 561 }, 562 { 563 name: "ok, rfc 2047, unknown charset", 564 header: `299 - "=?UTF-9?Q?This is a horsey: =F0=9F=90=8E?="`, 565 wantResult: WarningHeader{Code: 299, Agent: "-", Text: `=?UTF-9?Q?This is a horsey: =F0=9F=90=8E?=`}, 566 }, 567 { 568 name: "ok with spaces", 569 header: ` 299 - "text\"\\\a\b\c" `, 570 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 571 }, 572 { 573 name: "ok with date", 574 header: ` 299 - "text\"\\\a\b\c" "Tue, 15 Nov 1994 08:12:31 GMT" `, 575 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 576 }, 577 { 578 name: "ok with date and comma", 579 header: ` 299 - "text\"\\\a\b\c" "Tue, 15 Nov 1994 08:12:31 GMT" , `, 580 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 581 }, 582 { 583 name: "ok with comma", 584 header: ` 299 - "text\"\\\a\b\c" , `, 585 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 586 }, 587 { 588 name: "ok with date and comma and remainder", 589 header: ` 299 - "text\"\\\a\b\c" "Tue, 15 Nov 1994 08:12:31 GMT" , remainder `, 590 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 591 wantRemainder: "remainder", 592 }, 593 { 594 name: "ok with comma and remainder", 595 header: ` 299 - "text\"\\\a\b\c" ,remainder text,second remainder`, 596 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `text"\abc`}, 597 wantRemainder: "remainder text,second remainder", 598 }, 599 { 600 name: "ok with utf-8 content directly in warn-text", 601 header: ` 299 - "Test of Iñtërnâtiônàlizætiøn,💝🐹🌇⛔" `, 602 wantResult: WarningHeader{Code: 299, Agent: `-`, Text: `Test of Iñtërnâtiônàlizætiøn,💝🐹🌇⛔`}, 603 }, 604 } 605 for _, tt := range tests { 606 t.Run(tt.name, func(t *testing.T) { 607 gotResult, gotRemainder, err := ParseWarningHeader(tt.header) 608 switch { 609 case err == nil && len(tt.wantErr) > 0: 610 t.Errorf("ParseWarningHeader() no error, expected error %q", tt.wantErr) 611 return 612 case err != nil && len(tt.wantErr) == 0: 613 t.Errorf("ParseWarningHeader() error %q, expected no error", err) 614 return 615 case err != nil && len(tt.wantErr) > 0 && !strings.Contains(err.Error(), tt.wantErr): 616 t.Errorf("ParseWarningHeader() error %q, expected error %q", err, tt.wantErr) 617 return 618 } 619 if err != nil { 620 return 621 } 622 if !reflect.DeepEqual(gotResult, tt.wantResult) { 623 t.Errorf("ParseWarningHeader() gotResult = %#v, want %#v", gotResult, tt.wantResult) 624 } 625 if gotRemainder != tt.wantRemainder { 626 t.Errorf("ParseWarningHeader() gotRemainder = %v, want %v", gotRemainder, tt.wantRemainder) 627 } 628 }) 629 } 630 } 631 632 func TestNewWarningHeader(t *testing.T) { 633 tests := []struct { 634 name string 635 636 code int 637 agent string 638 text string 639 640 want string 641 wantErr string 642 }{ 643 // invalid cases 644 { 645 name: "code too low", 646 code: -1, 647 agent: `-`, 648 text: `example warning`, 649 wantErr: "between 0 and 999", 650 }, 651 { 652 name: "code too high", 653 code: 1000, 654 agent: `-`, 655 text: `example warning`, 656 wantErr: "between 0 and 999", 657 }, 658 { 659 name: "agent with space", 660 code: 299, 661 agent: `test agent`, 662 text: `example warning`, 663 wantErr: `agent must be valid`, 664 }, 665 { 666 name: "agent with newline", 667 code: 299, 668 agent: "test\nagent", 669 text: `example warning`, 670 wantErr: `agent must be valid`, 671 }, 672 { 673 name: "agent with backslash", 674 code: 299, 675 agent: `test\agent`, 676 text: `example warning`, 677 wantErr: `agent must be valid`, 678 }, 679 { 680 name: "agent with quote", 681 code: 299, 682 agent: `test"agent"`, 683 text: `example warning`, 684 wantErr: `agent must be valid`, 685 }, 686 { 687 name: "agent with control character", 688 code: 299, 689 agent: "test\u0000agent", 690 text: `example warning`, 691 wantErr: `agent must be valid`, 692 }, 693 { 694 name: "agent with non-UTF8", 695 code: 299, 696 agent: "test\xc5agent", 697 text: `example warning`, 698 wantErr: `agent must be valid`, 699 }, 700 { 701 name: "text with newline", 702 code: 299, 703 agent: `-`, 704 text: "Test of new\nline", 705 wantErr: "text must be valid", 706 }, 707 { 708 name: "text with control character", 709 code: 299, 710 agent: `-`, 711 text: "Test of control\u0000character", 712 wantErr: "text must be valid", 713 }, 714 { 715 name: "text with non-UTF8", 716 code: 299, 717 agent: `-`, 718 text: "Test of control\xc5character", 719 wantErr: "text must be valid", 720 }, 721 722 { 723 name: "valid empty text", 724 code: 299, 725 agent: `-`, 726 text: ``, 727 want: `299 - ""`, 728 }, 729 { 730 name: "valid empty agent", 731 code: 299, 732 agent: ``, 733 text: `example warning`, 734 want: `299 - "example warning"`, 735 }, 736 { 737 name: "valid low code", 738 code: 1, 739 agent: `-`, 740 text: `example warning`, 741 want: `001 - "example warning"`, 742 }, 743 { 744 name: "valid high code", 745 code: 999, 746 agent: `-`, 747 text: `example warning`, 748 want: `999 - "example warning"`, 749 }, 750 { 751 name: "valid utf-8", 752 code: 299, 753 agent: `-`, 754 text: `Test of "Iñtërnâtiônàlizætiøn,💝🐹🌇⛔"`, 755 want: `299 - "Test of \"Iñtërnâtiônàlizætiøn,💝🐹🌇⛔\""`, 756 }, 757 } 758 759 for _, tt := range tests { 760 t.Run(tt.name, func(t *testing.T) { 761 got, err := NewWarningHeader(tt.code, tt.agent, tt.text) 762 763 switch { 764 case err == nil && len(tt.wantErr) > 0: 765 t.Fatalf("ParseWarningHeader() no error, expected error %q", tt.wantErr) 766 case err != nil && len(tt.wantErr) == 0: 767 t.Fatalf("ParseWarningHeader() error %q, expected no error", err) 768 case err != nil && len(tt.wantErr) > 0 && !strings.Contains(err.Error(), tt.wantErr): 769 t.Fatalf("ParseWarningHeader() error %q, expected error %q", err, tt.wantErr) 770 } 771 if err != nil { 772 return 773 } 774 775 if got != tt.want { 776 t.Fatalf("NewWarningHeader() = %v, want %v", got, tt.want) 777 } 778 779 roundTrip, remaining, err := ParseWarningHeader(got) 780 if err != nil { 781 t.Fatalf("error roundtripping: %v", err) 782 } 783 if len(remaining) > 0 { 784 t.Fatalf("unexpected remainder roundtripping: %s", remaining) 785 } 786 agent := tt.agent 787 if len(agent) == 0 { 788 agent = "-" 789 } 790 expect := WarningHeader{Code: tt.code, Agent: agent, Text: tt.text} 791 if roundTrip != expect { 792 t.Fatalf("after round trip, want:\n%#v\ngot\n%#v", expect, roundTrip) 793 } 794 }) 795 } 796 } 797 798 func TestParseWarningHeaders(t *testing.T) { 799 tests := []struct { 800 name string 801 802 headers []string 803 804 want []WarningHeader 805 wantErrs []string 806 }{ 807 { 808 name: "empty", 809 headers: []string{}, 810 want: nil, 811 wantErrs: []string{}, 812 }, 813 { 814 name: "multi-header with error", 815 headers: []string{ 816 `299 - "warning 1.1",299 - "warning 1.2"`, 817 `299 - "warning 2", 299 - "warning unquoted`, 818 ` 299 - "warning 3.1" , 299 - "warning 3.2" `, 819 }, 820 want: []WarningHeader{ 821 {Code: 299, Agent: "-", Text: "warning 1.1"}, 822 {Code: 299, Agent: "-", Text: "warning 1.2"}, 823 {Code: 299, Agent: "-", Text: "warning 2"}, 824 {Code: 299, Agent: "-", Text: "warning 3.1"}, 825 {Code: 299, Agent: "-", Text: "warning 3.2"}, 826 }, 827 wantErrs: []string{"invalid warning header: invalid quoted string: missing closing quote"}, 828 }, 829 } 830 for _, tt := range tests { 831 t.Run(tt.name, func(t *testing.T) { 832 got, gotErrs := ParseWarningHeaders(tt.headers) 833 834 switch { 835 case len(gotErrs) != len(tt.wantErrs): 836 t.Fatalf("ParseWarningHeader() got %v, expected %v", gotErrs, tt.wantErrs) 837 case len(gotErrs) == len(tt.wantErrs) && len(gotErrs) > 0: 838 gotErrStrings := []string{} 839 for _, err := range gotErrs { 840 gotErrStrings = append(gotErrStrings, err.Error()) 841 } 842 if !reflect.DeepEqual(gotErrStrings, tt.wantErrs) { 843 t.Fatalf("ParseWarningHeader() got %v, expected %v", gotErrs, tt.wantErrs) 844 } 845 } 846 if len(gotErrs) > 0 { 847 return 848 } 849 850 if !reflect.DeepEqual(got, tt.want) { 851 t.Errorf("ParseWarningHeaders() got %#v, want %#v", got, tt.want) 852 } 853 }) 854 } 855 } 856 857 func TestIsProbableEOF(t *testing.T) { 858 tests := []struct { 859 name string 860 err error 861 expected bool 862 }{ 863 { 864 name: "with no error", 865 expected: false, 866 }, 867 { 868 name: "with EOF error", 869 err: io.EOF, 870 expected: true, 871 }, 872 { 873 name: "with unexpected EOF error", 874 err: io.ErrUnexpectedEOF, 875 expected: true, 876 }, 877 { 878 name: "with broken connection error", 879 err: fmt.Errorf("http: can't write HTTP request on broken connection"), 880 expected: true, 881 }, 882 { 883 name: "with server sent GOAWAY error", 884 err: fmt.Errorf("error foo - http2: server sent GOAWAY and closed the connection - error bar"), 885 expected: true, 886 }, 887 { 888 name: "with connection reset by peer error", 889 err: fmt.Errorf("error foo - connection reset by peer - error bar"), 890 expected: true, 891 }, 892 { 893 name: "with use of closed network connection error", 894 err: fmt.Errorf("error foo - Use of closed network connection - error bar"), 895 expected: true, 896 }, 897 { 898 name: "with url error", 899 err: &url.Error{ 900 Err: io.ErrUnexpectedEOF, 901 }, 902 expected: true, 903 }, 904 { 905 name: "with unrecognized error", 906 err: fmt.Errorf("error foo"), 907 expected: false, 908 }, 909 } 910 911 for _, test := range tests { 912 t.Run(test.name, func(t *testing.T) { 913 actual := IsProbableEOF(test.err) 914 assert.Equal(t, test.expected, actual) 915 }) 916 } 917 } 918 919 func TestReadIdleTimeoutSeconds(t *testing.T) { 920 t.Setenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS", "60") 921 if e, a := 60, readIdleTimeoutSeconds(); e != a { 922 t.Errorf("expected %d, got %d", e, a) 923 } 924 925 t.Setenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS", "illegal value") 926 if e, a := 30, readIdleTimeoutSeconds(); e != a { 927 t.Errorf("expected %d, got %d", e, a) 928 } 929 } 930 931 func TestPingTimeoutSeconds(t *testing.T) { 932 t.Setenv("HTTP2_PING_TIMEOUT_SECONDS", "60") 933 if e, a := 60, pingTimeoutSeconds(); e != a { 934 t.Errorf("expected %d, got %d", e, a) 935 } 936 937 t.Setenv("HTTP2_PING_TIMEOUT_SECONDS", "illegal value") 938 if e, a := 15, pingTimeoutSeconds(); e != a { 939 t.Errorf("expected %d, got %d", e, a) 940 } 941 } 942 943 func Benchmark_ParseQuotedString(b *testing.B) { 944 str := `"The quick brown" fox jumps over the lazy dog` 945 b.ReportAllocs() 946 b.ResetTimer() 947 for i := 0; i < b.N; i++ { 948 quoted, remainder, err := parseQuotedString(str) 949 if err != nil { 950 b.Errorf("Unexpected error %s", err) 951 } 952 if quoted != "The quick brown" { 953 b.Errorf("Unexpected quoted string %s", quoted) 954 } 955 if remainder != "fox jumps over the lazy dog" { 956 b.Errorf("Unexpected remainder string %s", quoted) 957 } 958 } 959 }