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  }