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