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