k8s.io/client-go@v0.22.2/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 Groups: []string{"one", "two"}, 206 Extra: map[string][]string{ 207 "first": {"A", "a"}, 208 "second": {"B", "b"}, 209 }, 210 }, 211 expected: map[string][]string{ 212 ImpersonateUserHeader: {"user"}, 213 ImpersonateGroupHeader: {"one", "two"}, 214 ImpersonateUserExtraHeaderPrefix + "First": {"A", "a"}, 215 ImpersonateUserExtraHeaderPrefix + "Second": {"B", "b"}, 216 }, 217 }, 218 { 219 name: "escape handling", 220 impersonationConfig: ImpersonationConfig{ 221 UserName: "user", 222 Extra: map[string][]string{ 223 "test.example.com/thing.thing": {"A", "a"}, 224 }, 225 }, 226 expected: map[string][]string{ 227 ImpersonateUserHeader: {"user"}, 228 ImpersonateUserExtraHeaderPrefix + `Test.example.com%2fthing.thing`: {"A", "a"}, 229 }, 230 }, 231 { 232 name: "double escape handling", 233 impersonationConfig: ImpersonationConfig{ 234 UserName: "user", 235 Extra: map[string][]string{ 236 "test.example.com/thing.thing%20another.thing": {"A", "a"}, 237 }, 238 }, 239 expected: map[string][]string{ 240 ImpersonateUserHeader: {"user"}, 241 ImpersonateUserExtraHeaderPrefix + `Test.example.com%2fthing.thing%2520another.thing`: {"A", "a"}, 242 }, 243 }, 244 } 245 246 for _, tc := range tcs { 247 rt := &testRoundTripper{} 248 req := &http.Request{ 249 Header: make(http.Header), 250 } 251 NewImpersonatingRoundTripper(tc.impersonationConfig, rt).RoundTrip(req) 252 253 for k, v := range rt.Request.Header { 254 expected, ok := tc.expected[k] 255 if !ok { 256 t.Errorf("%v missing %v=%v", tc.name, k, v) 257 continue 258 } 259 if !reflect.DeepEqual(expected, v) { 260 t.Errorf("%v expected %v: %v, got %v", tc.name, k, expected, v) 261 } 262 } 263 for k, v := range tc.expected { 264 expected, ok := rt.Request.Header[k] 265 if !ok { 266 t.Errorf("%v missing %v=%v", tc.name, k, v) 267 continue 268 } 269 if !reflect.DeepEqual(expected, v) { 270 t.Errorf("%v expected %v: %v, got %v", tc.name, k, expected, v) 271 } 272 } 273 } 274 } 275 276 func TestAuthProxyRoundTripper(t *testing.T) { 277 for n, tc := range map[string]struct { 278 username string 279 groups []string 280 extra map[string][]string 281 expectedExtra map[string][]string 282 }{ 283 "allfields": { 284 username: "user", 285 groups: []string{"groupA", "groupB"}, 286 extra: map[string][]string{ 287 "one": {"alpha", "bravo"}, 288 "two": {"charlie", "delta"}, 289 }, 290 expectedExtra: map[string][]string{ 291 "one": {"alpha", "bravo"}, 292 "two": {"charlie", "delta"}, 293 }, 294 }, 295 "escaped extra": { 296 username: "user", 297 groups: []string{"groupA", "groupB"}, 298 extra: map[string][]string{ 299 "one": {"alpha", "bravo"}, 300 "example.com/two": {"charlie", "delta"}, 301 }, 302 expectedExtra: map[string][]string{ 303 "one": {"alpha", "bravo"}, 304 "example.com%2ftwo": {"charlie", "delta"}, 305 }, 306 }, 307 "double escaped extra": { 308 username: "user", 309 groups: []string{"groupA", "groupB"}, 310 extra: map[string][]string{ 311 "one": {"alpha", "bravo"}, 312 "example.com/two%20three": {"charlie", "delta"}, 313 }, 314 expectedExtra: map[string][]string{ 315 "one": {"alpha", "bravo"}, 316 "example.com%2ftwo%2520three": {"charlie", "delta"}, 317 }, 318 }, 319 } { 320 rt := &testRoundTripper{} 321 req := &http.Request{} 322 NewAuthProxyRoundTripper(tc.username, tc.groups, tc.extra, rt).RoundTrip(req) 323 if rt.Request == nil { 324 t.Errorf("%s: unexpected nil request: %v", n, rt) 325 continue 326 } 327 if rt.Request == req { 328 t.Errorf("%s: round tripper should have copied request object: %#v", n, rt.Request) 329 continue 330 } 331 332 actualUsernames, ok := rt.Request.Header["X-Remote-User"] 333 if !ok { 334 t.Errorf("%s missing value", n) 335 continue 336 } 337 if e, a := []string{tc.username}, actualUsernames; !reflect.DeepEqual(e, a) { 338 t.Errorf("%s expected %v, got %v", n, e, a) 339 continue 340 } 341 actualGroups, ok := rt.Request.Header["X-Remote-Group"] 342 if !ok { 343 t.Errorf("%s missing value", n) 344 continue 345 } 346 if e, a := tc.groups, actualGroups; !reflect.DeepEqual(e, a) { 347 t.Errorf("%s expected %v, got %v", n, e, a) 348 continue 349 } 350 351 actualExtra := map[string][]string{} 352 for key, values := range rt.Request.Header { 353 if strings.HasPrefix(strings.ToLower(key), strings.ToLower("X-Remote-Extra-")) { 354 extraKey := strings.ToLower(key[len("X-Remote-Extra-"):]) 355 actualExtra[extraKey] = append(actualExtra[key], values...) 356 } 357 } 358 if e, a := tc.expectedExtra, actualExtra; !reflect.DeepEqual(e, a) { 359 t.Errorf("%s expected %v, got %v", n, e, a) 360 continue 361 } 362 } 363 } 364 365 // TestHeaderEscapeRoundTrip tests to see if foo == url.PathUnescape(headerEscape(foo)) 366 // This behavior is important for client -> API server transmission of extra values. 367 func TestHeaderEscapeRoundTrip(t *testing.T) { 368 t.Parallel() 369 testCases := []struct { 370 name string 371 key string 372 }{ 373 { 374 name: "alpha", 375 key: "alphabetical", 376 }, 377 { 378 name: "alphanumeric", 379 key: "alph4num3r1c", 380 }, 381 { 382 name: "percent encoded", 383 key: "percent%20encoded", 384 }, 385 { 386 name: "almost percent encoded", 387 key: "almost%zzpercent%xxencoded", 388 }, 389 { 390 name: "illegal char & percent encoding", 391 key: "example.com/percent%20encoded", 392 }, 393 { 394 name: "weird unicode stuff", 395 key: "example.com/ᛒᚥᛏᛖᚥᚢとロビン", 396 }, 397 { 398 name: "header legal chars", 399 key: "abc123!#$+.-_*\\^`~|'", 400 }, 401 { 402 name: "legal path, illegal header", 403 key: "@=:", 404 }, 405 } 406 for _, tc := range testCases { 407 t.Run(tc.name, func(t *testing.T) { 408 escaped := headerKeyEscape(tc.key) 409 unescaped, err := url.PathUnescape(escaped) 410 if err != nil { 411 t.Fatalf("url.PathUnescape(%q) returned error: %v", escaped, err) 412 } 413 if tc.key != unescaped { 414 t.Errorf("url.PathUnescape(headerKeyEscape(%q)) returned %q, wanted %q", tc.key, unescaped, tc.key) 415 } 416 }) 417 } 418 } 419 420 func TestDebuggingRoundTripper(t *testing.T) { 421 t.Parallel() 422 423 rawURL := "https://127.0.0.1:12345/api/v1/pods?limit=500" 424 req := &http.Request{ 425 Method: http.MethodGet, 426 Header: map[string][]string{ 427 "Authorization": {"bearer secretauthtoken"}, 428 "X-Test-Request": {"test"}, 429 }, 430 } 431 res := &http.Response{ 432 Status: "OK", 433 StatusCode: http.StatusOK, 434 Header: map[string][]string{ 435 "X-Test-Response": {"test"}, 436 }, 437 } 438 tcs := []struct { 439 levels []DebugLevel 440 expectedOutputLines []string 441 }{ 442 { 443 levels: []DebugLevel{DebugJustURL}, 444 expectedOutputLines: []string{fmt.Sprintf("%s %s", req.Method, rawURL)}, 445 }, 446 { 447 levels: []DebugLevel{DebugRequestHeaders}, 448 expectedOutputLines: func() []string { 449 lines := []string{fmt.Sprintf("Request Headers:\n")} 450 for key, values := range req.Header { 451 for _, value := range values { 452 if key == "Authorization" { 453 value = "bearer <masked>" 454 } 455 lines = append(lines, fmt.Sprintf(" %s: %s\n", key, value)) 456 } 457 } 458 return lines 459 }(), 460 }, 461 { 462 levels: []DebugLevel{DebugResponseHeaders}, 463 expectedOutputLines: func() []string { 464 lines := []string{fmt.Sprintf("Response Headers:\n")} 465 for key, values := range res.Header { 466 for _, value := range values { 467 lines = append(lines, fmt.Sprintf(" %s: %s\n", key, value)) 468 } 469 } 470 return lines 471 }(), 472 }, 473 { 474 levels: []DebugLevel{DebugURLTiming}, 475 expectedOutputLines: []string{fmt.Sprintf("%s %s %s", req.Method, rawURL, res.Status)}, 476 }, 477 { 478 levels: []DebugLevel{DebugResponseStatus}, 479 expectedOutputLines: []string{fmt.Sprintf("Response Status: %s", res.Status)}, 480 }, 481 { 482 levels: []DebugLevel{DebugCurlCommand}, 483 expectedOutputLines: []string{fmt.Sprintf("curl -v -X")}, 484 }, 485 } 486 487 for _, tc := range tcs { 488 // hijack the klog output 489 tmpWriteBuffer := bytes.NewBuffer(nil) 490 klog.SetOutput(tmpWriteBuffer) 491 klog.LogToStderr(false) 492 493 // parse rawURL 494 parsedURL, err := url.Parse(rawURL) 495 if err != nil { 496 t.Fatalf("url.Parse(%q) returned error: %v", rawURL, err) 497 } 498 req.URL = parsedURL 499 500 // execute the round tripper 501 rt := &testRoundTripper{ 502 Response: res, 503 } 504 NewDebuggingRoundTripper(rt, tc.levels...).RoundTrip(req) 505 506 // call Flush to ensure the text isn't still buffered 507 klog.Flush() 508 509 // check if klog's output contains the expected lines 510 actual := tmpWriteBuffer.String() 511 for _, expected := range tc.expectedOutputLines { 512 if !strings.Contains(actual, expected) { 513 t.Errorf("%q does not contain expected output %q", actual, expected) 514 } 515 } 516 } 517 }