k8s.io/client-go@v0.31.1/transport/round_trippers_test.go (about) 1 /* 2 Copyright 2014 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package transport 18 19 import ( 20 "bytes" 21 "fmt" 22 "net/http" 23 "net/url" 24 "reflect" 25 "strings" 26 "testing" 27 28 "k8s.io/klog/v2" 29 ) 30 31 type testRoundTripper struct { 32 Request *http.Request 33 Response *http.Response 34 Err error 35 } 36 37 func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 38 rt.Request = req 39 return rt.Response, rt.Err 40 } 41 42 func TestMaskValue(t *testing.T) { 43 tcs := []struct { 44 key string 45 value string 46 expected string 47 }{ 48 { 49 key: "Authorization", 50 value: "Basic YWxhZGRpbjpvcGVuc2VzYW1l", 51 expected: "Basic <masked>", 52 }, 53 { 54 key: "Authorization", 55 value: "basic", 56 expected: "basic", 57 }, 58 { 59 key: "Authorization", 60 value: "Basic", 61 expected: "Basic", 62 }, 63 { 64 key: "Authorization", 65 value: "Bearer cn389ncoiwuencr", 66 expected: "Bearer <masked>", 67 }, 68 { 69 key: "Authorization", 70 value: "Bearer", 71 expected: "Bearer", 72 }, 73 { 74 key: "Authorization", 75 value: "bearer", 76 expected: "bearer", 77 }, 78 { 79 key: "Authorization", 80 value: "bearer ", 81 expected: "bearer", 82 }, 83 { 84 key: "Authorization", 85 value: "Negotiate cn389ncoiwuencr", 86 expected: "Negotiate <masked>", 87 }, 88 { 89 key: "ABC", 90 value: "Negotiate cn389ncoiwuencr", 91 expected: "Negotiate cn389ncoiwuencr", 92 }, 93 { 94 key: "Authorization", 95 value: "Negotiate", 96 expected: "Negotiate", 97 }, 98 { 99 key: "Authorization", 100 value: "Negotiate ", 101 expected: "Negotiate", 102 }, 103 { 104 key: "Authorization", 105 value: "negotiate", 106 expected: "negotiate", 107 }, 108 { 109 key: "Authorization", 110 value: "abc cn389ncoiwuencr", 111 expected: "<masked>", 112 }, 113 { 114 key: "Authorization", 115 value: "", 116 expected: "", 117 }, 118 } 119 for _, tc := range tcs { 120 maskedValue := maskValue(tc.key, tc.value) 121 if tc.expected != maskedValue { 122 t.Errorf("unexpected value %s, given %s.", maskedValue, tc.value) 123 } 124 } 125 } 126 127 func TestBearerAuthRoundTripper(t *testing.T) { 128 rt := &testRoundTripper{} 129 req := &http.Request{} 130 NewBearerAuthRoundTripper("test", rt).RoundTrip(req) 131 if rt.Request == nil { 132 t.Fatalf("unexpected nil request: %v", rt) 133 } 134 if rt.Request == req { 135 t.Fatalf("round tripper should have copied request object: %#v", rt.Request) 136 } 137 if rt.Request.Header.Get("Authorization") != "Bearer test" { 138 t.Errorf("unexpected authorization header: %#v", rt.Request) 139 } 140 } 141 142 func TestBasicAuthRoundTripper(t *testing.T) { 143 for n, tc := range map[string]struct { 144 user string 145 pass string 146 }{ 147 "basic": {user: "user", pass: "pass"}, 148 "no pass": {user: "user"}, 149 } { 150 rt := &testRoundTripper{} 151 req := &http.Request{} 152 NewBasicAuthRoundTripper(tc.user, tc.pass, rt).RoundTrip(req) 153 if rt.Request == nil { 154 t.Fatalf("%s: unexpected nil request: %v", n, rt) 155 } 156 if rt.Request == req { 157 t.Fatalf("%s: round tripper should have copied request object: %#v", n, rt.Request) 158 } 159 if user, pass, found := rt.Request.BasicAuth(); !found || user != tc.user || pass != tc.pass { 160 t.Errorf("%s: unexpected authorization header: %#v", n, rt.Request) 161 } 162 } 163 } 164 165 func TestUserAgentRoundTripper(t *testing.T) { 166 rt := &testRoundTripper{} 167 req := &http.Request{ 168 Header: make(http.Header), 169 } 170 req.Header.Set("User-Agent", "other") 171 NewUserAgentRoundTripper("test", rt).RoundTrip(req) 172 if rt.Request == nil { 173 t.Fatalf("unexpected nil request: %v", rt) 174 } 175 if rt.Request != req { 176 t.Fatalf("round tripper should not have copied request object: %#v", rt.Request) 177 } 178 if rt.Request.Header.Get("User-Agent") != "other" { 179 t.Errorf("unexpected user agent header: %#v", rt.Request) 180 } 181 182 req = &http.Request{} 183 NewUserAgentRoundTripper("test", rt).RoundTrip(req) 184 if rt.Request == nil { 185 t.Fatalf("unexpected nil request: %v", rt) 186 } 187 if rt.Request == req { 188 t.Fatalf("round tripper should have copied request object: %#v", rt.Request) 189 } 190 if rt.Request.Header.Get("User-Agent") != "test" { 191 t.Errorf("unexpected user agent header: %#v", rt.Request) 192 } 193 } 194 195 func TestImpersonationRoundTripper(t *testing.T) { 196 tcs := []struct { 197 name string 198 impersonationConfig ImpersonationConfig 199 expected map[string][]string 200 }{ 201 { 202 name: "all", 203 impersonationConfig: ImpersonationConfig{ 204 UserName: "user", 205 UID: "uid-a", 206 Groups: []string{"one", "two"}, 207 Extra: map[string][]string{ 208 "first": {"A", "a"}, 209 "second": {"B", "b"}, 210 }, 211 }, 212 expected: map[string][]string{ 213 ImpersonateUserHeader: {"user"}, 214 ImpersonateUIDHeader: {"uid-a"}, 215 ImpersonateGroupHeader: {"one", "two"}, 216 ImpersonateUserExtraHeaderPrefix + "First": {"A", "a"}, 217 ImpersonateUserExtraHeaderPrefix + "Second": {"B", "b"}, 218 }, 219 }, 220 { 221 name: "username, groups and extra", 222 impersonationConfig: ImpersonationConfig{ 223 UserName: "user", 224 Groups: []string{"one", "two"}, 225 Extra: map[string][]string{ 226 "first": {"A", "a"}, 227 "second": {"B", "b"}, 228 }, 229 }, 230 expected: map[string][]string{ 231 ImpersonateUserHeader: {"user"}, 232 ImpersonateGroupHeader: {"one", "two"}, 233 ImpersonateUserExtraHeaderPrefix + "First": {"A", "a"}, 234 ImpersonateUserExtraHeaderPrefix + "Second": {"B", "b"}, 235 }, 236 }, 237 { 238 name: "username and uid", 239 impersonationConfig: ImpersonationConfig{ 240 UserName: "user", 241 UID: "uid-a", 242 }, 243 expected: map[string][]string{ 244 ImpersonateUserHeader: {"user"}, 245 ImpersonateUIDHeader: {"uid-a"}, 246 }, 247 }, 248 { 249 name: "escape handling", 250 impersonationConfig: ImpersonationConfig{ 251 UserName: "user", 252 Extra: map[string][]string{ 253 "test.example.com/thing.thing": {"A", "a"}, 254 }, 255 }, 256 expected: map[string][]string{ 257 ImpersonateUserHeader: {"user"}, 258 ImpersonateUserExtraHeaderPrefix + `Test.example.com%2fthing.thing`: {"A", "a"}, 259 }, 260 }, 261 { 262 name: "double escape handling", 263 impersonationConfig: ImpersonationConfig{ 264 UserName: "user", 265 Extra: map[string][]string{ 266 "test.example.com/thing.thing%20another.thing": {"A", "a"}, 267 }, 268 }, 269 expected: map[string][]string{ 270 ImpersonateUserHeader: {"user"}, 271 ImpersonateUserExtraHeaderPrefix + `Test.example.com%2fthing.thing%2520another.thing`: {"A", "a"}, 272 }, 273 }, 274 } 275 276 for _, tc := range tcs { 277 rt := &testRoundTripper{} 278 req := &http.Request{ 279 Header: make(http.Header), 280 } 281 NewImpersonatingRoundTripper(tc.impersonationConfig, rt).RoundTrip(req) 282 283 for k, v := range rt.Request.Header { 284 expected, ok := tc.expected[k] 285 if !ok { 286 t.Errorf("%v missing %v=%v", tc.name, k, v) 287 continue 288 } 289 if !reflect.DeepEqual(expected, v) { 290 t.Errorf("%v expected %v: %v, got %v", tc.name, k, expected, v) 291 } 292 } 293 for k, v := range tc.expected { 294 expected, ok := rt.Request.Header[k] 295 if !ok { 296 t.Errorf("%v missing %v=%v", tc.name, k, v) 297 continue 298 } 299 if !reflect.DeepEqual(expected, v) { 300 t.Errorf("%v expected %v: %v, got %v", tc.name, k, expected, v) 301 } 302 } 303 } 304 } 305 306 func TestAuthProxyRoundTripper(t *testing.T) { 307 for n, tc := range map[string]struct { 308 username string 309 groups []string 310 extra map[string][]string 311 expectedExtra map[string][]string 312 }{ 313 "allfields": { 314 username: "user", 315 groups: []string{"groupA", "groupB"}, 316 extra: map[string][]string{ 317 "one": {"alpha", "bravo"}, 318 "two": {"charlie", "delta"}, 319 }, 320 expectedExtra: map[string][]string{ 321 "one": {"alpha", "bravo"}, 322 "two": {"charlie", "delta"}, 323 }, 324 }, 325 "escaped extra": { 326 username: "user", 327 groups: []string{"groupA", "groupB"}, 328 extra: map[string][]string{ 329 "one": {"alpha", "bravo"}, 330 "example.com/two": {"charlie", "delta"}, 331 }, 332 expectedExtra: map[string][]string{ 333 "one": {"alpha", "bravo"}, 334 "example.com%2ftwo": {"charlie", "delta"}, 335 }, 336 }, 337 "double escaped extra": { 338 username: "user", 339 groups: []string{"groupA", "groupB"}, 340 extra: map[string][]string{ 341 "one": {"alpha", "bravo"}, 342 "example.com/two%20three": {"charlie", "delta"}, 343 }, 344 expectedExtra: map[string][]string{ 345 "one": {"alpha", "bravo"}, 346 "example.com%2ftwo%2520three": {"charlie", "delta"}, 347 }, 348 }, 349 } { 350 rt := &testRoundTripper{} 351 req := &http.Request{} 352 NewAuthProxyRoundTripper(tc.username, tc.groups, tc.extra, rt).RoundTrip(req) 353 if rt.Request == nil { 354 t.Errorf("%s: unexpected nil request: %v", n, rt) 355 continue 356 } 357 if rt.Request == req { 358 t.Errorf("%s: round tripper should have copied request object: %#v", n, rt.Request) 359 continue 360 } 361 362 actualUsernames, ok := rt.Request.Header["X-Remote-User"] 363 if !ok { 364 t.Errorf("%s missing value", n) 365 continue 366 } 367 if e, a := []string{tc.username}, actualUsernames; !reflect.DeepEqual(e, a) { 368 t.Errorf("%s expected %v, got %v", n, e, a) 369 continue 370 } 371 actualGroups, ok := rt.Request.Header["X-Remote-Group"] 372 if !ok { 373 t.Errorf("%s missing value", n) 374 continue 375 } 376 if e, a := tc.groups, actualGroups; !reflect.DeepEqual(e, a) { 377 t.Errorf("%s expected %v, got %v", n, e, a) 378 continue 379 } 380 381 actualExtra := map[string][]string{} 382 for key, values := range rt.Request.Header { 383 if strings.HasPrefix(strings.ToLower(key), strings.ToLower("X-Remote-Extra-")) { 384 extraKey := strings.ToLower(key[len("X-Remote-Extra-"):]) 385 actualExtra[extraKey] = append(actualExtra[key], values...) 386 } 387 } 388 if e, a := tc.expectedExtra, actualExtra; !reflect.DeepEqual(e, a) { 389 t.Errorf("%s expected %v, got %v", n, e, a) 390 continue 391 } 392 } 393 } 394 395 // TestHeaderEscapeRoundTrip tests to see if foo == url.PathUnescape(headerEscape(foo)) 396 // This behavior is important for client -> API server transmission of extra values. 397 func TestHeaderEscapeRoundTrip(t *testing.T) { 398 t.Parallel() 399 testCases := []struct { 400 name string 401 key string 402 }{ 403 { 404 name: "alpha", 405 key: "alphabetical", 406 }, 407 { 408 name: "alphanumeric", 409 key: "alph4num3r1c", 410 }, 411 { 412 name: "percent encoded", 413 key: "percent%20encoded", 414 }, 415 { 416 name: "almost percent encoded", 417 key: "almost%zzpercent%xxencoded", 418 }, 419 { 420 name: "illegal char & percent encoding", 421 key: "example.com/percent%20encoded", 422 }, 423 { 424 name: "weird unicode stuff", 425 key: "example.com/ᛒᚥᛏᛖᚥᚢとロビン", 426 }, 427 { 428 name: "header legal chars", 429 key: "abc123!#$+.-_*\\^`~|'", 430 }, 431 { 432 name: "legal path, illegal header", 433 key: "@=:", 434 }, 435 } 436 for _, tc := range testCases { 437 t.Run(tc.name, func(t *testing.T) { 438 escaped := headerKeyEscape(tc.key) 439 unescaped, err := url.PathUnescape(escaped) 440 if err != nil { 441 t.Fatalf("url.PathUnescape(%q) returned error: %v", escaped, err) 442 } 443 if tc.key != unescaped { 444 t.Errorf("url.PathUnescape(headerKeyEscape(%q)) returned %q, wanted %q", tc.key, unescaped, tc.key) 445 } 446 }) 447 } 448 } 449 450 func TestDebuggingRoundTripper(t *testing.T) { 451 t.Parallel() 452 453 rawURL := "https://127.0.0.1:12345/api/v1/pods?limit=500" 454 req := &http.Request{ 455 Method: http.MethodGet, 456 Header: map[string][]string{ 457 "Authorization": {"bearer secretauthtoken"}, 458 "X-Test-Request": {"test"}, 459 }, 460 } 461 res := &http.Response{ 462 Status: "OK", 463 StatusCode: http.StatusOK, 464 Header: map[string][]string{ 465 "X-Test-Response": {"test"}, 466 }, 467 } 468 tcs := []struct { 469 levels []DebugLevel 470 expectedOutputLines []string 471 }{ 472 { 473 levels: []DebugLevel{DebugJustURL}, 474 expectedOutputLines: []string{fmt.Sprintf("%s %s", req.Method, rawURL)}, 475 }, 476 { 477 levels: []DebugLevel{DebugRequestHeaders}, 478 expectedOutputLines: func() []string { 479 lines := []string{fmt.Sprintf("Request Headers:\n")} 480 for key, values := range req.Header { 481 for _, value := range values { 482 if key == "Authorization" { 483 value = "bearer <masked>" 484 } 485 lines = append(lines, fmt.Sprintf(" %s: %s\n", key, value)) 486 } 487 } 488 return lines 489 }(), 490 }, 491 { 492 levels: []DebugLevel{DebugResponseHeaders}, 493 expectedOutputLines: func() []string { 494 lines := []string{fmt.Sprintf("Response Headers:\n")} 495 for key, values := range res.Header { 496 for _, value := range values { 497 lines = append(lines, fmt.Sprintf(" %s: %s\n", key, value)) 498 } 499 } 500 return lines 501 }(), 502 }, 503 { 504 levels: []DebugLevel{DebugURLTiming}, 505 expectedOutputLines: []string{fmt.Sprintf("%s %s %s", req.Method, rawURL, res.Status)}, 506 }, 507 { 508 levels: []DebugLevel{DebugResponseStatus}, 509 expectedOutputLines: []string{fmt.Sprintf("Response Status: %s", res.Status)}, 510 }, 511 { 512 levels: []DebugLevel{DebugCurlCommand}, 513 expectedOutputLines: []string{fmt.Sprintf("curl -v -X")}, 514 }, 515 } 516 517 for _, tc := range tcs { 518 // hijack the klog output 519 tmpWriteBuffer := bytes.NewBuffer(nil) 520 klog.SetOutput(tmpWriteBuffer) 521 klog.LogToStderr(false) 522 523 // parse rawURL 524 parsedURL, err := url.Parse(rawURL) 525 if err != nil { 526 t.Fatalf("url.Parse(%q) returned error: %v", rawURL, err) 527 } 528 req.URL = parsedURL 529 530 // execute the round tripper 531 rt := &testRoundTripper{ 532 Response: res, 533 } 534 NewDebuggingRoundTripper(rt, tc.levels...).RoundTrip(req) 535 536 // call Flush to ensure the text isn't still buffered 537 klog.Flush() 538 539 // check if klog's output contains the expected lines 540 actual := tmpWriteBuffer.String() 541 for _, expected := range tc.expectedOutputLines { 542 if !strings.Contains(actual, expected) { 543 t.Errorf("%q does not contain expected output %q", actual, expected) 544 } 545 } 546 } 547 }