k8s.io/apimachinery@v0.29.2/pkg/util/proxy/upgradeaware.go (about) 1 /* 2 Copyright 2017 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 proxy 18 19 import ( 20 "bufio" 21 "bytes" 22 "fmt" 23 "io" 24 "log" 25 "net" 26 "net/http" 27 "net/http/httputil" 28 "net/url" 29 "os" 30 "strings" 31 "time" 32 33 "k8s.io/apimachinery/pkg/api/errors" 34 "k8s.io/apimachinery/pkg/util/httpstream" 35 utilnet "k8s.io/apimachinery/pkg/util/net" 36 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 37 38 "github.com/mxk/go-flowrate/flowrate" 39 "k8s.io/klog/v2" 40 ) 41 42 // UpgradeRequestRoundTripper provides an additional method to decorate a request 43 // with any authentication or other protocol level information prior to performing 44 // an upgrade on the server. Any response will be handled by the intercepting 45 // proxy. 46 type UpgradeRequestRoundTripper interface { 47 http.RoundTripper 48 // WrapRequest takes a valid HTTP request and returns a suitably altered version 49 // of request with any HTTP level values required to complete the request half of 50 // an upgrade on the server. It does not get a chance to see the response and 51 // should bypass any request side logic that expects to see the response. 52 WrapRequest(*http.Request) (*http.Request, error) 53 } 54 55 // UpgradeAwareHandler is a handler for proxy requests that may require an upgrade 56 type UpgradeAwareHandler struct { 57 // UpgradeRequired will reject non-upgrade connections if true. 58 UpgradeRequired bool 59 // Location is the location of the upstream proxy. It is used as the location to Dial on the upstream server 60 // for upgrade requests unless UseRequestLocationOnUpgrade is true. 61 Location *url.URL 62 // AppendLocationPath determines if the original path of the Location should be appended to the upstream proxy request path 63 AppendLocationPath bool 64 // Transport provides an optional round tripper to use to proxy. If nil, the default proxy transport is used 65 Transport http.RoundTripper 66 // UpgradeTransport, if specified, will be used as the backend transport when upgrade requests are provided. 67 // This allows clients to disable HTTP/2. 68 UpgradeTransport UpgradeRequestRoundTripper 69 // WrapTransport indicates whether the provided Transport should be wrapped with default proxy transport behavior (URL rewriting, X-Forwarded-* header setting) 70 WrapTransport bool 71 // UseRequestLocation will use the incoming request URL when talking to the backend server. 72 UseRequestLocation bool 73 // UseLocationHost overrides the HTTP host header in requests to the backend server to use the Host from Location. 74 // This will override the req.Host field of a request, while UseRequestLocation will override the req.URL field 75 // of a request. The req.URL.Host specifies the server to connect to, while the req.Host field 76 // specifies the Host header value to send in the HTTP request. If this is false, the incoming req.Host header will 77 // just be forwarded to the backend server. 78 UseLocationHost bool 79 // FlushInterval controls how often the standard HTTP proxy will flush content from the upstream. 80 FlushInterval time.Duration 81 // MaxBytesPerSec controls the maximum rate for an upstream connection. No rate is imposed if the value is zero. 82 MaxBytesPerSec int64 83 // Responder is passed errors that occur while setting up proxying. 84 Responder ErrorResponder 85 // Reject to forward redirect response 86 RejectForwardingRedirects bool 87 } 88 89 const defaultFlushInterval = 200 * time.Millisecond 90 91 // ErrorResponder abstracts error reporting to the proxy handler to remove the need to hardcode a particular 92 // error format. 93 type ErrorResponder interface { 94 Error(w http.ResponseWriter, req *http.Request, err error) 95 } 96 97 // SimpleErrorResponder is the legacy implementation of ErrorResponder for callers that only 98 // service a single request/response per proxy. 99 type SimpleErrorResponder interface { 100 Error(err error) 101 } 102 103 func NewErrorResponder(r SimpleErrorResponder) ErrorResponder { 104 return simpleResponder{r} 105 } 106 107 type simpleResponder struct { 108 responder SimpleErrorResponder 109 } 110 111 func (r simpleResponder) Error(w http.ResponseWriter, req *http.Request, err error) { 112 r.responder.Error(err) 113 } 114 115 // upgradeRequestRoundTripper implements proxy.UpgradeRequestRoundTripper. 116 type upgradeRequestRoundTripper struct { 117 http.RoundTripper 118 upgrader http.RoundTripper 119 } 120 121 var ( 122 _ UpgradeRequestRoundTripper = &upgradeRequestRoundTripper{} 123 _ utilnet.RoundTripperWrapper = &upgradeRequestRoundTripper{} 124 ) 125 126 // WrappedRoundTripper returns the round tripper that a caller would use. 127 func (rt *upgradeRequestRoundTripper) WrappedRoundTripper() http.RoundTripper { 128 return rt.RoundTripper 129 } 130 131 // WriteToRequest calls the nested upgrader and then copies the returned request 132 // fields onto the passed request. 133 func (rt *upgradeRequestRoundTripper) WrapRequest(req *http.Request) (*http.Request, error) { 134 resp, err := rt.upgrader.RoundTrip(req) 135 if err != nil { 136 return nil, err 137 } 138 return resp.Request, nil 139 } 140 141 // onewayRoundTripper captures the provided request - which is assumed to have 142 // been modified by other round trippers - and then returns a fake response. 143 type onewayRoundTripper struct{} 144 145 // RoundTrip returns a simple 200 OK response that captures the provided request. 146 func (onewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 147 return &http.Response{ 148 Status: "200 OK", 149 StatusCode: http.StatusOK, 150 Body: io.NopCloser(&bytes.Buffer{}), 151 Request: req, 152 }, nil 153 } 154 155 // MirrorRequest is a round tripper that can be called to get back the calling request as 156 // the core round tripper in a chain. 157 var MirrorRequest http.RoundTripper = onewayRoundTripper{} 158 159 // NewUpgradeRequestRoundTripper takes two round trippers - one for the underlying TCP connection, and 160 // one that is able to write headers to an HTTP request. The request rt is used to set the request headers 161 // and that is written to the underlying connection rt. 162 func NewUpgradeRequestRoundTripper(connection, request http.RoundTripper) UpgradeRequestRoundTripper { 163 return &upgradeRequestRoundTripper{ 164 RoundTripper: connection, 165 upgrader: request, 166 } 167 } 168 169 // normalizeLocation returns the result of parsing the full URL, with scheme set to http if missing 170 func normalizeLocation(location *url.URL) *url.URL { 171 normalized, _ := url.Parse(location.String()) 172 if len(normalized.Scheme) == 0 { 173 normalized.Scheme = "http" 174 } 175 return normalized 176 } 177 178 // NewUpgradeAwareHandler creates a new proxy handler with a default flush interval. Responder is required for returning 179 // errors to the caller. 180 func NewUpgradeAwareHandler(location *url.URL, transport http.RoundTripper, wrapTransport, upgradeRequired bool, responder ErrorResponder) *UpgradeAwareHandler { 181 return &UpgradeAwareHandler{ 182 Location: normalizeLocation(location), 183 Transport: transport, 184 WrapTransport: wrapTransport, 185 UpgradeRequired: upgradeRequired, 186 FlushInterval: defaultFlushInterval, 187 Responder: responder, 188 } 189 } 190 191 func proxyRedirectsforRootPath(path string, w http.ResponseWriter, req *http.Request) bool { 192 redirect := false 193 method := req.Method 194 195 // From pkg/genericapiserver/endpoints/handlers/proxy.go#ServeHTTP: 196 // Redirect requests with an empty path to a location that ends with a '/' 197 // This is essentially a hack for https://issue.k8s.io/4958. 198 // Note: Keep this code after tryUpgrade to not break that flow. 199 if len(path) == 0 && (method == http.MethodGet || method == http.MethodHead) { 200 var queryPart string 201 if len(req.URL.RawQuery) > 0 { 202 queryPart = "?" + req.URL.RawQuery 203 } 204 w.Header().Set("Location", req.URL.Path+"/"+queryPart) 205 w.WriteHeader(http.StatusMovedPermanently) 206 redirect = true 207 } 208 return redirect 209 } 210 211 // ServeHTTP handles the proxy request 212 func (h *UpgradeAwareHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 213 if h.tryUpgrade(w, req) { 214 return 215 } 216 if h.UpgradeRequired { 217 h.Responder.Error(w, req, errors.NewBadRequest("Upgrade request required")) 218 return 219 } 220 221 loc := *h.Location 222 loc.RawQuery = req.URL.RawQuery 223 224 // If original request URL ended in '/', append a '/' at the end of the 225 // of the proxy URL 226 if !strings.HasSuffix(loc.Path, "/") && strings.HasSuffix(req.URL.Path, "/") { 227 loc.Path += "/" 228 } 229 230 proxyRedirect := proxyRedirectsforRootPath(loc.Path, w, req) 231 if proxyRedirect { 232 return 233 } 234 235 if h.Transport == nil || h.WrapTransport { 236 h.Transport = h.defaultProxyTransport(req.URL, h.Transport) 237 } 238 239 // WithContext creates a shallow clone of the request with the same context. 240 newReq := req.WithContext(req.Context()) 241 newReq.Header = utilnet.CloneHeader(req.Header) 242 if !h.UseRequestLocation { 243 newReq.URL = &loc 244 } 245 if h.UseLocationHost { 246 // exchanging req.Host with the backend location is necessary for backends that act on the HTTP host header (e.g. API gateways), 247 // because req.Host has preference over req.URL.Host in filling this header field 248 newReq.Host = h.Location.Host 249 } 250 251 // create the target location to use for the reverse proxy 252 reverseProxyLocation := &url.URL{Scheme: h.Location.Scheme, Host: h.Location.Host} 253 if h.AppendLocationPath { 254 reverseProxyLocation.Path = h.Location.Path 255 } 256 257 proxy := httputil.NewSingleHostReverseProxy(reverseProxyLocation) 258 proxy.Transport = h.Transport 259 proxy.FlushInterval = h.FlushInterval 260 proxy.ErrorLog = log.New(noSuppressPanicError{}, "", log.LstdFlags) 261 if h.RejectForwardingRedirects { 262 oldModifyResponse := proxy.ModifyResponse 263 proxy.ModifyResponse = func(response *http.Response) error { 264 code := response.StatusCode 265 if code >= 300 && code <= 399 && len(response.Header.Get("Location")) > 0 { 266 // close the original response 267 response.Body.Close() 268 msg := "the backend attempted to redirect this request, which is not permitted" 269 // replace the response 270 *response = http.Response{ 271 StatusCode: http.StatusBadGateway, 272 Status: fmt.Sprintf("%d %s", response.StatusCode, http.StatusText(response.StatusCode)), 273 Body: io.NopCloser(strings.NewReader(msg)), 274 ContentLength: int64(len(msg)), 275 } 276 } else { 277 if oldModifyResponse != nil { 278 if err := oldModifyResponse(response); err != nil { 279 return err 280 } 281 } 282 } 283 return nil 284 } 285 } 286 if h.Responder != nil { 287 // if an optional error interceptor/responder was provided wire it 288 // the custom responder might be used for providing a unified error reporting 289 // or supporting retry mechanisms by not sending non-fatal errors to the clients 290 proxy.ErrorHandler = h.Responder.Error 291 } 292 proxy.ServeHTTP(w, newReq) 293 } 294 295 type noSuppressPanicError struct{} 296 297 func (noSuppressPanicError) Write(p []byte) (n int, err error) { 298 // skip "suppressing panic for copyResponse error in test; copy error" error message 299 // that ends up in CI tests on each kube-apiserver termination as noise and 300 // everybody thinks this is fatal. 301 if strings.Contains(string(p), "suppressing panic") { 302 return len(p), nil 303 } 304 return os.Stderr.Write(p) 305 } 306 307 // tryUpgrade returns true if the request was handled. 308 func (h *UpgradeAwareHandler) tryUpgrade(w http.ResponseWriter, req *http.Request) bool { 309 if !httpstream.IsUpgradeRequest(req) { 310 klog.V(6).Infof("Request was not an upgrade") 311 return false 312 } 313 314 var ( 315 backendConn net.Conn 316 rawResponse []byte 317 err error 318 ) 319 320 location := *h.Location 321 if h.UseRequestLocation { 322 location = *req.URL 323 location.Scheme = h.Location.Scheme 324 location.Host = h.Location.Host 325 if h.AppendLocationPath { 326 location.Path = singleJoiningSlash(h.Location.Path, location.Path) 327 } 328 } 329 330 clone := utilnet.CloneRequest(req) 331 // Only append X-Forwarded-For in the upgrade path, since httputil.NewSingleHostReverseProxy 332 // handles this in the non-upgrade path. 333 utilnet.AppendForwardedForHeader(clone) 334 klog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n Headers: %v", &location, clone.Header) 335 if h.UseLocationHost { 336 clone.Host = h.Location.Host 337 } 338 clone.URL = &location 339 backendConn, err = h.DialForUpgrade(clone) 340 if err != nil { 341 klog.V(6).Infof("Proxy connection error: %v", err) 342 h.Responder.Error(w, req, err) 343 return true 344 } 345 defer backendConn.Close() 346 347 // determine the http response code from the backend by reading from rawResponse+backendConn 348 backendHTTPResponse, headerBytes, err := getResponse(io.MultiReader(bytes.NewReader(rawResponse), backendConn)) 349 if err != nil { 350 klog.V(6).Infof("Proxy connection error: %v", err) 351 h.Responder.Error(w, req, err) 352 return true 353 } 354 if len(headerBytes) > len(rawResponse) { 355 // we read beyond the bytes stored in rawResponse, update rawResponse to the full set of bytes read from the backend 356 rawResponse = headerBytes 357 } 358 359 // If the backend did not upgrade the request, return an error to the client. If the response was 360 // an error, the error is forwarded directly after the connection is hijacked. Otherwise, just 361 // return a generic error here. 362 if backendHTTPResponse.StatusCode != http.StatusSwitchingProtocols && backendHTTPResponse.StatusCode < 400 { 363 err := fmt.Errorf("invalid upgrade response: status code %d", backendHTTPResponse.StatusCode) 364 klog.Errorf("Proxy upgrade error: %v", err) 365 h.Responder.Error(w, req, err) 366 return true 367 } 368 369 // Once the connection is hijacked, the ErrorResponder will no longer work, so 370 // hijacking should be the last step in the upgrade. 371 requestHijacker, ok := w.(http.Hijacker) 372 if !ok { 373 klog.V(6).Infof("Unable to hijack response writer: %T", w) 374 h.Responder.Error(w, req, fmt.Errorf("request connection cannot be hijacked: %T", w)) 375 return true 376 } 377 requestHijackedConn, _, err := requestHijacker.Hijack() 378 if err != nil { 379 klog.V(6).Infof("Unable to hijack response: %v", err) 380 h.Responder.Error(w, req, fmt.Errorf("error hijacking connection: %v", err)) 381 return true 382 } 383 defer requestHijackedConn.Close() 384 385 if backendHTTPResponse.StatusCode != http.StatusSwitchingProtocols { 386 // If the backend did not upgrade the request, echo the response from the backend to the client and return, closing the connection. 387 klog.V(6).Infof("Proxy upgrade error, status code %d", backendHTTPResponse.StatusCode) 388 // set read/write deadlines 389 deadline := time.Now().Add(10 * time.Second) 390 backendConn.SetReadDeadline(deadline) 391 requestHijackedConn.SetWriteDeadline(deadline) 392 // write the response to the client 393 err := backendHTTPResponse.Write(requestHijackedConn) 394 if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { 395 klog.Errorf("Error proxying data from backend to client: %v", err) 396 } 397 // Indicate we handled the request 398 return true 399 } 400 401 // Forward raw response bytes back to client. 402 if len(rawResponse) > 0 { 403 klog.V(6).Infof("Writing %d bytes to hijacked connection", len(rawResponse)) 404 if _, err = requestHijackedConn.Write(rawResponse); err != nil { 405 utilruntime.HandleError(fmt.Errorf("Error proxying response from backend to client: %v", err)) 406 } 407 } 408 409 // Proxy the connection. This is bidirectional, so we need a goroutine 410 // to copy in each direction. Once one side of the connection exits, we 411 // exit the function which performs cleanup and in the process closes 412 // the other half of the connection in the defer. 413 writerComplete := make(chan struct{}) 414 readerComplete := make(chan struct{}) 415 416 go func() { 417 var writer io.WriteCloser 418 if h.MaxBytesPerSec > 0 { 419 writer = flowrate.NewWriter(backendConn, h.MaxBytesPerSec) 420 } else { 421 writer = backendConn 422 } 423 _, err := io.Copy(writer, requestHijackedConn) 424 if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { 425 klog.Errorf("Error proxying data from client to backend: %v", err) 426 } 427 close(writerComplete) 428 }() 429 430 go func() { 431 var reader io.ReadCloser 432 if h.MaxBytesPerSec > 0 { 433 reader = flowrate.NewReader(backendConn, h.MaxBytesPerSec) 434 } else { 435 reader = backendConn 436 } 437 _, err := io.Copy(requestHijackedConn, reader) 438 if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { 439 klog.Errorf("Error proxying data from backend to client: %v", err) 440 } 441 close(readerComplete) 442 }() 443 444 // Wait for one half the connection to exit. Once it does the defer will 445 // clean up the other half of the connection. 446 select { 447 case <-writerComplete: 448 case <-readerComplete: 449 } 450 klog.V(6).Infof("Disconnecting from backend proxy %s\n Headers: %v", &location, clone.Header) 451 452 return true 453 } 454 455 // FIXME: Taken from net/http/httputil/reverseproxy.go as singleJoiningSlash is not exported to be re-used. 456 // See-also: https://github.com/golang/go/issues/44290 457 func singleJoiningSlash(a, b string) string { 458 aslash := strings.HasSuffix(a, "/") 459 bslash := strings.HasPrefix(b, "/") 460 switch { 461 case aslash && bslash: 462 return a + b[1:] 463 case !aslash && !bslash: 464 return a + "/" + b 465 } 466 return a + b 467 } 468 469 func (h *UpgradeAwareHandler) DialForUpgrade(req *http.Request) (net.Conn, error) { 470 if h.UpgradeTransport == nil { 471 return dial(req, h.Transport) 472 } 473 updatedReq, err := h.UpgradeTransport.WrapRequest(req) 474 if err != nil { 475 return nil, err 476 } 477 return dial(updatedReq, h.UpgradeTransport) 478 } 479 480 // getResponseCode reads a http response from the given reader, returns the response, 481 // the bytes read from the reader, and any error encountered 482 func getResponse(r io.Reader) (*http.Response, []byte, error) { 483 rawResponse := bytes.NewBuffer(make([]byte, 0, 256)) 484 // Save the bytes read while reading the response headers into the rawResponse buffer 485 resp, err := http.ReadResponse(bufio.NewReader(io.TeeReader(r, rawResponse)), nil) 486 if err != nil { 487 return nil, nil, err 488 } 489 // return the http response and the raw bytes consumed from the reader in the process 490 return resp, rawResponse.Bytes(), nil 491 } 492 493 // dial dials the backend at req.URL and writes req to it. 494 func dial(req *http.Request, transport http.RoundTripper) (net.Conn, error) { 495 conn, err := DialURL(req.Context(), req.URL, transport) 496 if err != nil { 497 return nil, fmt.Errorf("error dialing backend: %v", err) 498 } 499 500 if err = req.Write(conn); err != nil { 501 conn.Close() 502 return nil, fmt.Errorf("error sending request: %v", err) 503 } 504 505 return conn, err 506 } 507 508 func (h *UpgradeAwareHandler) defaultProxyTransport(url *url.URL, internalTransport http.RoundTripper) http.RoundTripper { 509 scheme := url.Scheme 510 host := url.Host 511 suffix := h.Location.Path 512 if strings.HasSuffix(url.Path, "/") && !strings.HasSuffix(suffix, "/") { 513 suffix += "/" 514 } 515 pathPrepend := strings.TrimSuffix(url.Path, suffix) 516 rewritingTransport := &Transport{ 517 Scheme: scheme, 518 Host: host, 519 PathPrepend: pathPrepend, 520 RoundTripper: internalTransport, 521 } 522 return &corsRemovingTransport{ 523 RoundTripper: rewritingTransport, 524 } 525 } 526 527 // corsRemovingTransport is a wrapper for an internal transport. It removes CORS headers 528 // from the internal response. 529 // Implements pkg/util/net.RoundTripperWrapper 530 type corsRemovingTransport struct { 531 http.RoundTripper 532 } 533 534 var _ = utilnet.RoundTripperWrapper(&corsRemovingTransport{}) 535 536 func (rt *corsRemovingTransport) RoundTrip(req *http.Request) (*http.Response, error) { 537 resp, err := rt.RoundTripper.RoundTrip(req) 538 if err != nil { 539 return nil, err 540 } 541 removeCORSHeaders(resp) 542 return resp, nil 543 } 544 545 func (rt *corsRemovingTransport) WrappedRoundTripper() http.RoundTripper { 546 return rt.RoundTripper 547 } 548 549 // removeCORSHeaders strip CORS headers sent from the backend 550 // This should be called on all responses before returning 551 func removeCORSHeaders(resp *http.Response) { 552 resp.Header.Del("Access-Control-Allow-Credentials") 553 resp.Header.Del("Access-Control-Allow-Headers") 554 resp.Header.Del("Access-Control-Allow-Methods") 555 resp.Header.Del("Access-Control-Allow-Origin") 556 }