...

Source file src/k8s.io/client-go/transport/round_trippers_test.go

Documentation: k8s.io/client-go/transport

     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  }
   548  

View as plain text