1
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
396
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
519 tmpWriteBuffer := bytes.NewBuffer(nil)
520 klog.SetOutput(tmpWriteBuffer)
521 klog.LogToStderr(false)
522
523
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
531 rt := &testRoundTripper{
532 Response: res,
533 }
534 NewDebuggingRoundTripper(rt, tc.levels...).RoundTrip(req)
535
536
537 klog.Flush()
538
539
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 }
548
View as plain text