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  }