k8s.io/client-go@v0.22.2/transport/round_trippers.go (about) 1 /* 2 Copyright 2015 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 "fmt" 21 "net/http" 22 "strings" 23 "time" 24 25 "golang.org/x/oauth2" 26 27 utilnet "k8s.io/apimachinery/pkg/util/net" 28 "k8s.io/klog/v2" 29 ) 30 31 // HTTPWrappersForConfig wraps a round tripper with any relevant layered 32 // behavior from the config. Exposed to allow more clients that need HTTP-like 33 // behavior but then must hijack the underlying connection (like WebSocket or 34 // HTTP2 clients). Pure HTTP clients should use the RoundTripper returned from 35 // New. 36 func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) { 37 if config.WrapTransport != nil { 38 rt = config.WrapTransport(rt) 39 } 40 41 rt = DebugWrappers(rt) 42 43 // Set authentication wrappers 44 switch { 45 case config.HasBasicAuth() && config.HasTokenAuth(): 46 return nil, fmt.Errorf("username/password or bearer token may be set, but not both") 47 case config.HasTokenAuth(): 48 var err error 49 rt, err = NewBearerAuthWithRefreshRoundTripper(config.BearerToken, config.BearerTokenFile, rt) 50 if err != nil { 51 return nil, err 52 } 53 case config.HasBasicAuth(): 54 rt = NewBasicAuthRoundTripper(config.Username, config.Password, rt) 55 } 56 if len(config.UserAgent) > 0 { 57 rt = NewUserAgentRoundTripper(config.UserAgent, rt) 58 } 59 if len(config.Impersonate.UserName) > 0 || 60 len(config.Impersonate.Groups) > 0 || 61 len(config.Impersonate.Extra) > 0 { 62 rt = NewImpersonatingRoundTripper(config.Impersonate, rt) 63 } 64 return rt, nil 65 } 66 67 // DebugWrappers wraps a round tripper and logs based on the current log level. 68 func DebugWrappers(rt http.RoundTripper) http.RoundTripper { 69 switch { 70 case bool(klog.V(9).Enabled()): 71 rt = NewDebuggingRoundTripper(rt, DebugCurlCommand, DebugURLTiming, DebugResponseHeaders) 72 case bool(klog.V(8).Enabled()): 73 rt = NewDebuggingRoundTripper(rt, DebugJustURL, DebugRequestHeaders, DebugResponseStatus, DebugResponseHeaders) 74 case bool(klog.V(7).Enabled()): 75 rt = NewDebuggingRoundTripper(rt, DebugJustURL, DebugRequestHeaders, DebugResponseStatus) 76 case bool(klog.V(6).Enabled()): 77 rt = NewDebuggingRoundTripper(rt, DebugURLTiming) 78 } 79 80 return rt 81 } 82 83 type authProxyRoundTripper struct { 84 username string 85 groups []string 86 extra map[string][]string 87 88 rt http.RoundTripper 89 } 90 91 // NewAuthProxyRoundTripper provides a roundtripper which will add auth proxy fields to requests for 92 // authentication terminating proxy cases 93 // assuming you pull the user from the context: 94 // username is the user.Info.GetName() of the user 95 // groups is the user.Info.GetGroups() of the user 96 // extra is the user.Info.GetExtra() of the user 97 // extra can contain any additional information that the authenticator 98 // thought was interesting, for example authorization scopes. 99 // In order to faithfully round-trip through an impersonation flow, these keys 100 // MUST be lowercase. 101 func NewAuthProxyRoundTripper(username string, groups []string, extra map[string][]string, rt http.RoundTripper) http.RoundTripper { 102 return &authProxyRoundTripper{ 103 username: username, 104 groups: groups, 105 extra: extra, 106 rt: rt, 107 } 108 } 109 110 func (rt *authProxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 111 req = utilnet.CloneRequest(req) 112 SetAuthProxyHeaders(req, rt.username, rt.groups, rt.extra) 113 114 return rt.rt.RoundTrip(req) 115 } 116 117 // SetAuthProxyHeaders stomps the auth proxy header fields. It mutates its argument. 118 func SetAuthProxyHeaders(req *http.Request, username string, groups []string, extra map[string][]string) { 119 req.Header.Del("X-Remote-User") 120 req.Header.Del("X-Remote-Group") 121 for key := range req.Header { 122 if strings.HasPrefix(strings.ToLower(key), strings.ToLower("X-Remote-Extra-")) { 123 req.Header.Del(key) 124 } 125 } 126 127 req.Header.Set("X-Remote-User", username) 128 for _, group := range groups { 129 req.Header.Add("X-Remote-Group", group) 130 } 131 for key, values := range extra { 132 for _, value := range values { 133 req.Header.Add("X-Remote-Extra-"+headerKeyEscape(key), value) 134 } 135 } 136 } 137 138 func (rt *authProxyRoundTripper) CancelRequest(req *http.Request) { 139 tryCancelRequest(rt.WrappedRoundTripper(), req) 140 } 141 142 func (rt *authProxyRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } 143 144 type userAgentRoundTripper struct { 145 agent string 146 rt http.RoundTripper 147 } 148 149 // NewUserAgentRoundTripper will add User-Agent header to a request unless it has already been set. 150 func NewUserAgentRoundTripper(agent string, rt http.RoundTripper) http.RoundTripper { 151 return &userAgentRoundTripper{agent, rt} 152 } 153 154 func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 155 if len(req.Header.Get("User-Agent")) != 0 { 156 return rt.rt.RoundTrip(req) 157 } 158 req = utilnet.CloneRequest(req) 159 req.Header.Set("User-Agent", rt.agent) 160 return rt.rt.RoundTrip(req) 161 } 162 163 func (rt *userAgentRoundTripper) CancelRequest(req *http.Request) { 164 tryCancelRequest(rt.WrappedRoundTripper(), req) 165 } 166 167 func (rt *userAgentRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } 168 169 type basicAuthRoundTripper struct { 170 username string 171 password string `datapolicy:"password"` 172 rt http.RoundTripper 173 } 174 175 // NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a 176 // request unless it has already been set. 177 func NewBasicAuthRoundTripper(username, password string, rt http.RoundTripper) http.RoundTripper { 178 return &basicAuthRoundTripper{username, password, rt} 179 } 180 181 func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 182 if len(req.Header.Get("Authorization")) != 0 { 183 return rt.rt.RoundTrip(req) 184 } 185 req = utilnet.CloneRequest(req) 186 req.SetBasicAuth(rt.username, rt.password) 187 return rt.rt.RoundTrip(req) 188 } 189 190 func (rt *basicAuthRoundTripper) CancelRequest(req *http.Request) { 191 tryCancelRequest(rt.WrappedRoundTripper(), req) 192 } 193 194 func (rt *basicAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } 195 196 // These correspond to the headers used in pkg/apis/authentication. We don't want the package dependency, 197 // but you must not change the values. 198 const ( 199 // ImpersonateUserHeader is used to impersonate a particular user during an API server request 200 ImpersonateUserHeader = "Impersonate-User" 201 202 // ImpersonateGroupHeader is used to impersonate a particular group during an API server request. 203 // It can be repeated multiplied times for multiple groups. 204 ImpersonateGroupHeader = "Impersonate-Group" 205 206 // ImpersonateUserExtraHeaderPrefix is a prefix for a header used to impersonate an entry in the 207 // extra map[string][]string for user.Info. The key for the `extra` map is suffix. 208 // The same key can be repeated multiple times to have multiple elements in the slice under a single key. 209 // For instance: 210 // Impersonate-Extra-Foo: one 211 // Impersonate-Extra-Foo: two 212 // results in extra["Foo"] = []string{"one", "two"} 213 ImpersonateUserExtraHeaderPrefix = "Impersonate-Extra-" 214 ) 215 216 type impersonatingRoundTripper struct { 217 impersonate ImpersonationConfig 218 delegate http.RoundTripper 219 } 220 221 // NewImpersonatingRoundTripper will add an Act-As header to a request unless it has already been set. 222 func NewImpersonatingRoundTripper(impersonate ImpersonationConfig, delegate http.RoundTripper) http.RoundTripper { 223 return &impersonatingRoundTripper{impersonate, delegate} 224 } 225 226 func (rt *impersonatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 227 // use the user header as marker for the rest. 228 if len(req.Header.Get(ImpersonateUserHeader)) != 0 { 229 return rt.delegate.RoundTrip(req) 230 } 231 req = utilnet.CloneRequest(req) 232 req.Header.Set(ImpersonateUserHeader, rt.impersonate.UserName) 233 234 for _, group := range rt.impersonate.Groups { 235 req.Header.Add(ImpersonateGroupHeader, group) 236 } 237 for k, vv := range rt.impersonate.Extra { 238 for _, v := range vv { 239 req.Header.Add(ImpersonateUserExtraHeaderPrefix+headerKeyEscape(k), v) 240 } 241 } 242 243 return rt.delegate.RoundTrip(req) 244 } 245 246 func (rt *impersonatingRoundTripper) CancelRequest(req *http.Request) { 247 tryCancelRequest(rt.WrappedRoundTripper(), req) 248 } 249 250 func (rt *impersonatingRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.delegate } 251 252 type bearerAuthRoundTripper struct { 253 bearer string 254 source oauth2.TokenSource 255 rt http.RoundTripper 256 } 257 258 // NewBearerAuthRoundTripper adds the provided bearer token to a request 259 // unless the authorization header has already been set. 260 func NewBearerAuthRoundTripper(bearer string, rt http.RoundTripper) http.RoundTripper { 261 return &bearerAuthRoundTripper{bearer, nil, rt} 262 } 263 264 // NewBearerAuthWithRefreshRoundTripper adds the provided bearer token to a request 265 // unless the authorization header has already been set. 266 // If tokenFile is non-empty, it is periodically read, 267 // and the last successfully read content is used as the bearer token. 268 // If tokenFile is non-empty and bearer is empty, the tokenFile is read 269 // immediately to populate the initial bearer token. 270 func NewBearerAuthWithRefreshRoundTripper(bearer string, tokenFile string, rt http.RoundTripper) (http.RoundTripper, error) { 271 if len(tokenFile) == 0 { 272 return &bearerAuthRoundTripper{bearer, nil, rt}, nil 273 } 274 source := NewCachedFileTokenSource(tokenFile) 275 if len(bearer) == 0 { 276 token, err := source.Token() 277 if err != nil { 278 return nil, err 279 } 280 bearer = token.AccessToken 281 } 282 return &bearerAuthRoundTripper{bearer, source, rt}, nil 283 } 284 285 func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 286 if len(req.Header.Get("Authorization")) != 0 { 287 return rt.rt.RoundTrip(req) 288 } 289 290 req = utilnet.CloneRequest(req) 291 token := rt.bearer 292 if rt.source != nil { 293 if refreshedToken, err := rt.source.Token(); err == nil { 294 token = refreshedToken.AccessToken 295 } 296 } 297 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 298 return rt.rt.RoundTrip(req) 299 } 300 301 func (rt *bearerAuthRoundTripper) CancelRequest(req *http.Request) { 302 tryCancelRequest(rt.WrappedRoundTripper(), req) 303 } 304 305 func (rt *bearerAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt } 306 307 // requestInfo keeps track of information about a request/response combination 308 type requestInfo struct { 309 RequestHeaders http.Header `datapolicy:"token"` 310 RequestVerb string 311 RequestURL string 312 313 ResponseStatus string 314 ResponseHeaders http.Header 315 ResponseErr error 316 317 Duration time.Duration 318 } 319 320 // newRequestInfo creates a new RequestInfo based on an http request 321 func newRequestInfo(req *http.Request) *requestInfo { 322 return &requestInfo{ 323 RequestURL: req.URL.String(), 324 RequestVerb: req.Method, 325 RequestHeaders: req.Header, 326 } 327 } 328 329 // complete adds information about the response to the requestInfo 330 func (r *requestInfo) complete(response *http.Response, err error) { 331 if err != nil { 332 r.ResponseErr = err 333 return 334 } 335 r.ResponseStatus = response.Status 336 r.ResponseHeaders = response.Header 337 } 338 339 // toCurl returns a string that can be run as a command in a terminal (minus the body) 340 func (r *requestInfo) toCurl() string { 341 headers := "" 342 for key, values := range r.RequestHeaders { 343 for _, value := range values { 344 value = maskValue(key, value) 345 headers += fmt.Sprintf(` -H %q`, fmt.Sprintf("%s: %s", key, value)) 346 } 347 } 348 349 return fmt.Sprintf("curl -v -X%s %s '%s'", r.RequestVerb, headers, r.RequestURL) 350 } 351 352 // debuggingRoundTripper will display information about the requests passing 353 // through it based on what is configured 354 type debuggingRoundTripper struct { 355 delegatedRoundTripper http.RoundTripper 356 levels map[DebugLevel]bool 357 } 358 359 // DebugLevel is used to enable debugging of certain 360 // HTTP requests and responses fields via the debuggingRoundTripper. 361 type DebugLevel int 362 363 const ( 364 // DebugJustURL will add to the debug output HTTP requests method and url. 365 DebugJustURL DebugLevel = iota 366 // DebugURLTiming will add to the debug output the duration of HTTP requests. 367 DebugURLTiming 368 // DebugCurlCommand will add to the debug output the curl command equivalent to the 369 // HTTP request. 370 DebugCurlCommand 371 // DebugRequestHeaders will add to the debug output the HTTP requests headers. 372 DebugRequestHeaders 373 // DebugResponseStatus will add to the debug output the HTTP response status. 374 DebugResponseStatus 375 // DebugResponseHeaders will add to the debug output the HTTP response headers. 376 DebugResponseHeaders 377 ) 378 379 // NewDebuggingRoundTripper allows to display in the logs output debug information 380 // on the API requests performed by the client. 381 func NewDebuggingRoundTripper(rt http.RoundTripper, levels ...DebugLevel) http.RoundTripper { 382 drt := &debuggingRoundTripper{ 383 delegatedRoundTripper: rt, 384 levels: make(map[DebugLevel]bool, len(levels)), 385 } 386 for _, v := range levels { 387 drt.levels[v] = true 388 } 389 return drt 390 } 391 392 func (rt *debuggingRoundTripper) CancelRequest(req *http.Request) { 393 tryCancelRequest(rt.WrappedRoundTripper(), req) 394 } 395 396 var knownAuthTypes = map[string]bool{ 397 "bearer": true, 398 "basic": true, 399 "negotiate": true, 400 } 401 402 // maskValue masks credential content from authorization headers 403 // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization 404 func maskValue(key string, value string) string { 405 if !strings.EqualFold(key, "Authorization") { 406 return value 407 } 408 if len(value) == 0 { 409 return "" 410 } 411 var authType string 412 if i := strings.Index(value, " "); i > 0 { 413 authType = value[0:i] 414 } else { 415 authType = value 416 } 417 if !knownAuthTypes[strings.ToLower(authType)] { 418 return "<masked>" 419 } 420 if len(value) > len(authType)+1 { 421 value = authType + " <masked>" 422 } else { 423 value = authType 424 } 425 return value 426 } 427 428 func (rt *debuggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 429 reqInfo := newRequestInfo(req) 430 431 if rt.levels[DebugJustURL] { 432 klog.Infof("%s %s", reqInfo.RequestVerb, reqInfo.RequestURL) 433 } 434 if rt.levels[DebugCurlCommand] { 435 klog.Infof("%s", reqInfo.toCurl()) 436 } 437 if rt.levels[DebugRequestHeaders] { 438 klog.Info("Request Headers:") 439 for key, values := range reqInfo.RequestHeaders { 440 for _, value := range values { 441 value = maskValue(key, value) 442 klog.Infof(" %s: %s", key, value) 443 } 444 } 445 } 446 447 startTime := time.Now() 448 response, err := rt.delegatedRoundTripper.RoundTrip(req) 449 reqInfo.Duration = time.Since(startTime) 450 451 reqInfo.complete(response, err) 452 453 if rt.levels[DebugURLTiming] { 454 klog.Infof("%s %s %s in %d milliseconds", reqInfo.RequestVerb, reqInfo.RequestURL, reqInfo.ResponseStatus, reqInfo.Duration.Nanoseconds()/int64(time.Millisecond)) 455 } 456 if rt.levels[DebugResponseStatus] { 457 klog.Infof("Response Status: %s in %d milliseconds", reqInfo.ResponseStatus, reqInfo.Duration.Nanoseconds()/int64(time.Millisecond)) 458 } 459 if rt.levels[DebugResponseHeaders] { 460 klog.Info("Response Headers:") 461 for key, values := range reqInfo.ResponseHeaders { 462 for _, value := range values { 463 klog.Infof(" %s: %s", key, value) 464 } 465 } 466 } 467 468 return response, err 469 } 470 471 func (rt *debuggingRoundTripper) WrappedRoundTripper() http.RoundTripper { 472 return rt.delegatedRoundTripper 473 } 474 475 func legalHeaderByte(b byte) bool { 476 return int(b) < len(legalHeaderKeyBytes) && legalHeaderKeyBytes[b] 477 } 478 479 func shouldEscape(b byte) bool { 480 // url.PathUnescape() returns an error if any '%' is not followed by two 481 // hexadecimal digits, so we'll intentionally encode it. 482 return !legalHeaderByte(b) || b == '%' 483 } 484 485 func headerKeyEscape(key string) string { 486 buf := strings.Builder{} 487 for i := 0; i < len(key); i++ { 488 b := key[i] 489 if shouldEscape(b) { 490 // %-encode bytes that should be escaped: 491 // https://tools.ietf.org/html/rfc3986#section-2.1 492 fmt.Fprintf(&buf, "%%%02X", b) 493 continue 494 } 495 buf.WriteByte(b) 496 } 497 return buf.String() 498 } 499 500 // legalHeaderKeyBytes was copied from net/http/lex.go's isTokenTable. 501 // See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators 502 var legalHeaderKeyBytes = [127]bool{ 503 '%': true, 504 '!': true, 505 '#': true, 506 '$': true, 507 '&': true, 508 '\'': true, 509 '*': true, 510 '+': true, 511 '-': true, 512 '.': true, 513 '0': true, 514 '1': true, 515 '2': true, 516 '3': true, 517 '4': true, 518 '5': true, 519 '6': true, 520 '7': true, 521 '8': true, 522 '9': true, 523 'A': true, 524 'B': true, 525 'C': true, 526 'D': true, 527 'E': true, 528 'F': true, 529 'G': true, 530 'H': true, 531 'I': true, 532 'J': true, 533 'K': true, 534 'L': true, 535 'M': true, 536 'N': true, 537 'O': true, 538 'P': true, 539 'Q': true, 540 'R': true, 541 'S': true, 542 'T': true, 543 'U': true, 544 'W': true, 545 'V': true, 546 'X': true, 547 'Y': true, 548 'Z': true, 549 '^': true, 550 '_': true, 551 '`': true, 552 'a': true, 553 'b': true, 554 'c': true, 555 'd': true, 556 'e': true, 557 'f': true, 558 'g': true, 559 'h': true, 560 'i': true, 561 'j': true, 562 'k': true, 563 'l': true, 564 'm': true, 565 'n': true, 566 'o': true, 567 'p': true, 568 'q': true, 569 'r': true, 570 's': true, 571 't': true, 572 'u': true, 573 'v': true, 574 'w': true, 575 'x': true, 576 'y': true, 577 'z': true, 578 '|': true, 579 '~': true, 580 }