blob: a05208d924d3be7f65dc5f6d7d9b054b671e9703 [file] [log] [blame]
Matteo Scandoloa4285862020-12-01 18:10:10 -08001/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package transport
18
19import (
20 "fmt"
21 "net/http"
22 "strings"
23 "time"
24
25 "golang.org/x/oauth2"
26 "k8s.io/klog/v2"
27
28 utilnet "k8s.io/apimachinery/pkg/util/net"
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.
36func 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.
68func 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
83type 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.
101func 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
110func (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.
118func 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
138func (rt *authProxyRoundTripper) CancelRequest(req *http.Request) {
139 tryCancelRequest(rt.WrappedRoundTripper(), req)
140}
141
142func (rt *authProxyRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt }
143
144type userAgentRoundTripper struct {
145 agent string
146 rt http.RoundTripper
147}
148
149func NewUserAgentRoundTripper(agent string, rt http.RoundTripper) http.RoundTripper {
150 return &userAgentRoundTripper{agent, rt}
151}
152
153func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
154 if len(req.Header.Get("User-Agent")) != 0 {
155 return rt.rt.RoundTrip(req)
156 }
157 req = utilnet.CloneRequest(req)
158 req.Header.Set("User-Agent", rt.agent)
159 return rt.rt.RoundTrip(req)
160}
161
162func (rt *userAgentRoundTripper) CancelRequest(req *http.Request) {
163 tryCancelRequest(rt.WrappedRoundTripper(), req)
164}
165
166func (rt *userAgentRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt }
167
168type basicAuthRoundTripper struct {
169 username string
170 password string
171 rt http.RoundTripper
172}
173
174// NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a
175// request unless it has already been set.
176func NewBasicAuthRoundTripper(username, password string, rt http.RoundTripper) http.RoundTripper {
177 return &basicAuthRoundTripper{username, password, rt}
178}
179
180func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
181 if len(req.Header.Get("Authorization")) != 0 {
182 return rt.rt.RoundTrip(req)
183 }
184 req = utilnet.CloneRequest(req)
185 req.SetBasicAuth(rt.username, rt.password)
186 return rt.rt.RoundTrip(req)
187}
188
189func (rt *basicAuthRoundTripper) CancelRequest(req *http.Request) {
190 tryCancelRequest(rt.WrappedRoundTripper(), req)
191}
192
193func (rt *basicAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt }
194
195// These correspond to the headers used in pkg/apis/authentication. We don't want the package dependency,
196// but you must not change the values.
197const (
198 // ImpersonateUserHeader is used to impersonate a particular user during an API server request
199 ImpersonateUserHeader = "Impersonate-User"
200
201 // ImpersonateGroupHeader is used to impersonate a particular group during an API server request.
202 // It can be repeated multiplied times for multiple groups.
203 ImpersonateGroupHeader = "Impersonate-Group"
204
205 // ImpersonateUserExtraHeaderPrefix is a prefix for a header used to impersonate an entry in the
206 // extra map[string][]string for user.Info. The key for the `extra` map is suffix.
207 // The same key can be repeated multiple times to have multiple elements in the slice under a single key.
208 // For instance:
209 // Impersonate-Extra-Foo: one
210 // Impersonate-Extra-Foo: two
211 // results in extra["Foo"] = []string{"one", "two"}
212 ImpersonateUserExtraHeaderPrefix = "Impersonate-Extra-"
213)
214
215type impersonatingRoundTripper struct {
216 impersonate ImpersonationConfig
217 delegate http.RoundTripper
218}
219
220// NewImpersonatingRoundTripper will add an Act-As header to a request unless it has already been set.
221func NewImpersonatingRoundTripper(impersonate ImpersonationConfig, delegate http.RoundTripper) http.RoundTripper {
222 return &impersonatingRoundTripper{impersonate, delegate}
223}
224
225func (rt *impersonatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
226 // use the user header as marker for the rest.
227 if len(req.Header.Get(ImpersonateUserHeader)) != 0 {
228 return rt.delegate.RoundTrip(req)
229 }
230 req = utilnet.CloneRequest(req)
231 req.Header.Set(ImpersonateUserHeader, rt.impersonate.UserName)
232
233 for _, group := range rt.impersonate.Groups {
234 req.Header.Add(ImpersonateGroupHeader, group)
235 }
236 for k, vv := range rt.impersonate.Extra {
237 for _, v := range vv {
238 req.Header.Add(ImpersonateUserExtraHeaderPrefix+headerKeyEscape(k), v)
239 }
240 }
241
242 return rt.delegate.RoundTrip(req)
243}
244
245func (rt *impersonatingRoundTripper) CancelRequest(req *http.Request) {
246 tryCancelRequest(rt.WrappedRoundTripper(), req)
247}
248
249func (rt *impersonatingRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.delegate }
250
251type bearerAuthRoundTripper struct {
252 bearer string
253 source oauth2.TokenSource
254 rt http.RoundTripper
255}
256
257// NewBearerAuthRoundTripper adds the provided bearer token to a request
258// unless the authorization header has already been set.
259func NewBearerAuthRoundTripper(bearer string, rt http.RoundTripper) http.RoundTripper {
260 return &bearerAuthRoundTripper{bearer, nil, rt}
261}
262
263// NewBearerAuthRoundTripper adds the provided bearer token to a request
264// unless the authorization header has already been set.
265// If tokenFile is non-empty, it is periodically read,
266// and the last successfully read content is used as the bearer token.
267// If tokenFile is non-empty and bearer is empty, the tokenFile is read
268// immediately to populate the initial bearer token.
269func NewBearerAuthWithRefreshRoundTripper(bearer string, tokenFile string, rt http.RoundTripper) (http.RoundTripper, error) {
270 if len(tokenFile) == 0 {
271 return &bearerAuthRoundTripper{bearer, nil, rt}, nil
272 }
273 source := NewCachedFileTokenSource(tokenFile)
274 if len(bearer) == 0 {
275 token, err := source.Token()
276 if err != nil {
277 return nil, err
278 }
279 bearer = token.AccessToken
280 }
281 return &bearerAuthRoundTripper{bearer, source, rt}, nil
282}
283
284func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
285 if len(req.Header.Get("Authorization")) != 0 {
286 return rt.rt.RoundTrip(req)
287 }
288
289 req = utilnet.CloneRequest(req)
290 token := rt.bearer
291 if rt.source != nil {
292 if refreshedToken, err := rt.source.Token(); err == nil {
293 token = refreshedToken.AccessToken
294 }
295 }
296 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
297 return rt.rt.RoundTrip(req)
298}
299
300func (rt *bearerAuthRoundTripper) CancelRequest(req *http.Request) {
301 tryCancelRequest(rt.WrappedRoundTripper(), req)
302}
303
304func (rt *bearerAuthRoundTripper) WrappedRoundTripper() http.RoundTripper { return rt.rt }
305
306// requestInfo keeps track of information about a request/response combination
307type requestInfo struct {
308 RequestHeaders http.Header
309 RequestVerb string
310 RequestURL string
311
312 ResponseStatus string
313 ResponseHeaders http.Header
314 ResponseErr error
315
316 Duration time.Duration
317}
318
319// newRequestInfo creates a new RequestInfo based on an http request
320func newRequestInfo(req *http.Request) *requestInfo {
321 return &requestInfo{
322 RequestURL: req.URL.String(),
323 RequestVerb: req.Method,
324 RequestHeaders: req.Header,
325 }
326}
327
328// complete adds information about the response to the requestInfo
329func (r *requestInfo) complete(response *http.Response, err error) {
330 if err != nil {
331 r.ResponseErr = err
332 return
333 }
334 r.ResponseStatus = response.Status
335 r.ResponseHeaders = response.Header
336}
337
338// toCurl returns a string that can be run as a command in a terminal (minus the body)
339func (r *requestInfo) toCurl() string {
340 headers := ""
341 for key, values := range r.RequestHeaders {
342 for _, value := range values {
343 headers += fmt.Sprintf(` -H %q`, fmt.Sprintf("%s: %s", key, value))
344 }
345 }
346
347 return fmt.Sprintf("curl -k -v -X%s %s '%s'", r.RequestVerb, headers, r.RequestURL)
348}
349
350// debuggingRoundTripper will display information about the requests passing
351// through it based on what is configured
352type debuggingRoundTripper struct {
353 delegatedRoundTripper http.RoundTripper
354
355 levels map[debugLevel]bool
356}
357
358type debugLevel int
359
360const (
361 debugJustURL debugLevel = iota
362 debugURLTiming
363 debugCurlCommand
364 debugRequestHeaders
365 debugResponseStatus
366 debugResponseHeaders
367)
368
369func newDebuggingRoundTripper(rt http.RoundTripper, levels ...debugLevel) *debuggingRoundTripper {
370 drt := &debuggingRoundTripper{
371 delegatedRoundTripper: rt,
372 levels: make(map[debugLevel]bool, len(levels)),
373 }
374 for _, v := range levels {
375 drt.levels[v] = true
376 }
377 return drt
378}
379
380func (rt *debuggingRoundTripper) CancelRequest(req *http.Request) {
381 tryCancelRequest(rt.WrappedRoundTripper(), req)
382}
383
384var knownAuthTypes = map[string]bool{
385 "bearer": true,
386 "basic": true,
387 "negotiate": true,
388}
389
390// maskValue masks credential content from authorization headers
391// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
392func maskValue(key string, value string) string {
393 if !strings.EqualFold(key, "Authorization") {
394 return value
395 }
396 if len(value) == 0 {
397 return ""
398 }
399 var authType string
400 if i := strings.Index(value, " "); i > 0 {
401 authType = value[0:i]
402 } else {
403 authType = value
404 }
405 if !knownAuthTypes[strings.ToLower(authType)] {
406 return "<masked>"
407 }
408 if len(value) > len(authType)+1 {
409 value = authType + " <masked>"
410 } else {
411 value = authType
412 }
413 return value
414}
415
416func (rt *debuggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
417 reqInfo := newRequestInfo(req)
418
419 if rt.levels[debugJustURL] {
420 klog.Infof("%s %s", reqInfo.RequestVerb, reqInfo.RequestURL)
421 }
422 if rt.levels[debugCurlCommand] {
423 klog.Infof("%s", reqInfo.toCurl())
424
425 }
426 if rt.levels[debugRequestHeaders] {
427 klog.Infof("Request Headers:")
428 for key, values := range reqInfo.RequestHeaders {
429 for _, value := range values {
430 value = maskValue(key, value)
431 klog.Infof(" %s: %s", key, value)
432 }
433 }
434 }
435
436 startTime := time.Now()
437 response, err := rt.delegatedRoundTripper.RoundTrip(req)
438 reqInfo.Duration = time.Since(startTime)
439
440 reqInfo.complete(response, err)
441
442 if rt.levels[debugURLTiming] {
443 klog.Infof("%s %s %s in %d milliseconds", reqInfo.RequestVerb, reqInfo.RequestURL, reqInfo.ResponseStatus, reqInfo.Duration.Nanoseconds()/int64(time.Millisecond))
444 }
445 if rt.levels[debugResponseStatus] {
446 klog.Infof("Response Status: %s in %d milliseconds", reqInfo.ResponseStatus, reqInfo.Duration.Nanoseconds()/int64(time.Millisecond))
447 }
448 if rt.levels[debugResponseHeaders] {
449 klog.Infof("Response Headers:")
450 for key, values := range reqInfo.ResponseHeaders {
451 for _, value := range values {
452 klog.Infof(" %s: %s", key, value)
453 }
454 }
455 }
456
457 return response, err
458}
459
460func (rt *debuggingRoundTripper) WrappedRoundTripper() http.RoundTripper {
461 return rt.delegatedRoundTripper
462}
463
464func legalHeaderByte(b byte) bool {
465 return int(b) < len(legalHeaderKeyBytes) && legalHeaderKeyBytes[b]
466}
467
468func shouldEscape(b byte) bool {
469 // url.PathUnescape() returns an error if any '%' is not followed by two
470 // hexadecimal digits, so we'll intentionally encode it.
471 return !legalHeaderByte(b) || b == '%'
472}
473
474func headerKeyEscape(key string) string {
475 buf := strings.Builder{}
476 for i := 0; i < len(key); i++ {
477 b := key[i]
478 if shouldEscape(b) {
479 // %-encode bytes that should be escaped:
480 // https://tools.ietf.org/html/rfc3986#section-2.1
481 fmt.Fprintf(&buf, "%%%02X", b)
482 continue
483 }
484 buf.WriteByte(b)
485 }
486 return buf.String()
487}
488
489// legalHeaderKeyBytes was copied from net/http/lex.go's isTokenTable.
490// See https://httpwg.github.io/specs/rfc7230.html#rule.token.separators
491var legalHeaderKeyBytes = [127]bool{
492 '%': true,
493 '!': true,
494 '#': true,
495 '$': true,
496 '&': true,
497 '\'': true,
498 '*': true,
499 '+': true,
500 '-': true,
501 '.': true,
502 '0': true,
503 '1': true,
504 '2': true,
505 '3': true,
506 '4': true,
507 '5': true,
508 '6': true,
509 '7': true,
510 '8': true,
511 '9': true,
512 'A': true,
513 'B': true,
514 'C': true,
515 'D': true,
516 'E': true,
517 'F': true,
518 'G': true,
519 'H': true,
520 'I': true,
521 'J': true,
522 'K': true,
523 'L': true,
524 'M': true,
525 'N': true,
526 'O': true,
527 'P': true,
528 'Q': true,
529 'R': true,
530 'S': true,
531 'T': true,
532 'U': true,
533 'W': true,
534 'V': true,
535 'X': true,
536 'Y': true,
537 'Z': true,
538 '^': true,
539 '_': true,
540 '`': true,
541 'a': true,
542 'b': true,
543 'c': true,
544 'd': true,
545 'e': true,
546 'f': true,
547 'g': true,
548 'h': true,
549 'i': true,
550 'j': true,
551 'k': true,
552 'l': true,
553 'm': true,
554 'n': true,
555 'o': true,
556 'p': true,
557 'q': true,
558 'r': true,
559 's': true,
560 't': true,
561 'u': true,
562 'v': true,
563 'w': true,
564 'x': true,
565 'y': true,
566 'z': true,
567 '|': true,
568 '~': true,
569}