...

Source file src/k8s.io/apimachinery/pkg/util/httpstream/spdy/roundtripper.go

Documentation: k8s.io/apimachinery/pkg/util/httpstream/spdy

     1  /*
     2  Copyright 2015 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 spdy
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"crypto/tls"
    23  	"encoding/base64"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"net"
    28  	"net/http"
    29  	"net/http/httputil"
    30  	"net/url"
    31  	"strings"
    32  	"time"
    33  
    34  	"golang.org/x/net/proxy"
    35  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/apimachinery/pkg/runtime/serializer"
    39  	"k8s.io/apimachinery/pkg/util/httpstream"
    40  	utilnet "k8s.io/apimachinery/pkg/util/net"
    41  	apiproxy "k8s.io/apimachinery/pkg/util/proxy"
    42  	"k8s.io/apimachinery/third_party/forked/golang/netutil"
    43  )
    44  
    45  // SpdyRoundTripper knows how to upgrade an HTTP request to one that supports
    46  // multiplexed streams. After RoundTrip() is invoked, Conn will be set
    47  // and usable. SpdyRoundTripper implements the UpgradeRoundTripper interface.
    48  type SpdyRoundTripper struct {
    49  	//tlsConfig holds the TLS configuration settings to use when connecting
    50  	//to the remote server.
    51  	tlsConfig *tls.Config
    52  
    53  	/* TODO according to http://golang.org/pkg/net/http/#RoundTripper, a RoundTripper
    54  	   must be safe for use by multiple concurrent goroutines. If this is absolutely
    55  	   necessary, we could keep a map from http.Request to net.Conn. In practice,
    56  	   a client will create an http.Client, set the transport to a new insteace of
    57  	   SpdyRoundTripper, and use it a single time, so this hopefully won't be an issue.
    58  	*/
    59  	// conn is the underlying network connection to the remote server.
    60  	conn net.Conn
    61  
    62  	// Dialer is the dialer used to connect.  Used if non-nil.
    63  	Dialer *net.Dialer
    64  
    65  	// proxier knows which proxy to use given a request, defaults to http.ProxyFromEnvironment
    66  	// Used primarily for mocking the proxy discovery in tests.
    67  	proxier func(req *http.Request) (*url.URL, error)
    68  
    69  	// pingPeriod is a period for sending Ping frames over established
    70  	// connections.
    71  	pingPeriod time.Duration
    72  
    73  	// upgradeTransport is an optional substitute for dialing if present. This field is
    74  	// mutually exclusive with the "tlsConfig", "Dialer", and "proxier".
    75  	upgradeTransport http.RoundTripper
    76  }
    77  
    78  var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
    79  var _ httpstream.UpgradeRoundTripper = &SpdyRoundTripper{}
    80  var _ utilnet.Dialer = &SpdyRoundTripper{}
    81  
    82  // NewRoundTripper creates a new SpdyRoundTripper that will use the specified
    83  // tlsConfig.
    84  func NewRoundTripper(tlsConfig *tls.Config) (*SpdyRoundTripper, error) {
    85  	return NewRoundTripperWithConfig(RoundTripperConfig{
    86  		TLS:              tlsConfig,
    87  		UpgradeTransport: nil,
    88  	})
    89  }
    90  
    91  // NewRoundTripperWithProxy creates a new SpdyRoundTripper that will use the
    92  // specified tlsConfig and proxy func.
    93  func NewRoundTripperWithProxy(tlsConfig *tls.Config, proxier func(*http.Request) (*url.URL, error)) (*SpdyRoundTripper, error) {
    94  	return NewRoundTripperWithConfig(RoundTripperConfig{
    95  		TLS:              tlsConfig,
    96  		Proxier:          proxier,
    97  		UpgradeTransport: nil,
    98  	})
    99  }
   100  
   101  // NewRoundTripperWithConfig creates a new SpdyRoundTripper with the specified
   102  // configuration. Returns an error if the SpdyRoundTripper is misconfigured.
   103  func NewRoundTripperWithConfig(cfg RoundTripperConfig) (*SpdyRoundTripper, error) {
   104  	// Process UpgradeTransport, which is mutually exclusive to TLSConfig and Proxier.
   105  	if cfg.UpgradeTransport != nil {
   106  		if cfg.TLS != nil || cfg.Proxier != nil {
   107  			return nil, fmt.Errorf("SpdyRoundTripper: UpgradeTransport is mutually exclusive to TLSConfig or Proxier")
   108  		}
   109  		tlsConfig, err := utilnet.TLSClientConfig(cfg.UpgradeTransport)
   110  		if err != nil {
   111  			return nil, fmt.Errorf("SpdyRoundTripper: Unable to retrieve TLSConfig from  UpgradeTransport: %v", err)
   112  		}
   113  		cfg.TLS = tlsConfig
   114  	}
   115  	if cfg.Proxier == nil {
   116  		cfg.Proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
   117  	}
   118  	return &SpdyRoundTripper{
   119  		tlsConfig:        cfg.TLS,
   120  		proxier:          cfg.Proxier,
   121  		pingPeriod:       cfg.PingPeriod,
   122  		upgradeTransport: cfg.UpgradeTransport,
   123  	}, nil
   124  }
   125  
   126  // RoundTripperConfig is a set of options for an SpdyRoundTripper.
   127  type RoundTripperConfig struct {
   128  	// TLS configuration used by the round tripper if UpgradeTransport not present.
   129  	TLS *tls.Config
   130  	// Proxier is a proxy function invoked on each request. Optional.
   131  	Proxier func(*http.Request) (*url.URL, error)
   132  	// PingPeriod is a period for sending SPDY Pings on the connection.
   133  	// Optional.
   134  	PingPeriod time.Duration
   135  	// UpgradeTransport is a subtitute transport used for dialing. If set,
   136  	// this field will be used instead of "TLS" and "Proxier" for connection creation.
   137  	// Optional.
   138  	UpgradeTransport http.RoundTripper
   139  }
   140  
   141  // TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
   142  // proxying with a spdy roundtripper.
   143  func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config {
   144  	return s.tlsConfig
   145  }
   146  
   147  // Dial implements k8s.io/apimachinery/pkg/util/net.Dialer.
   148  func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) {
   149  	var conn net.Conn
   150  	var err error
   151  	if s.upgradeTransport != nil {
   152  		conn, err = apiproxy.DialURL(req.Context(), req.URL, s.upgradeTransport)
   153  	} else {
   154  		conn, err = s.dial(req)
   155  	}
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	if err := req.Write(conn); err != nil {
   161  		conn.Close()
   162  		return nil, err
   163  	}
   164  
   165  	return conn, nil
   166  }
   167  
   168  // dial dials the host specified by req, using TLS if appropriate, optionally
   169  // using a proxy server if one is configured via environment variables.
   170  func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) {
   171  	proxyURL, err := s.proxier(req)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	if proxyURL == nil {
   177  		return s.dialWithoutProxy(req.Context(), req.URL)
   178  	}
   179  
   180  	switch proxyURL.Scheme {
   181  	case "socks5":
   182  		return s.dialWithSocks5Proxy(req, proxyURL)
   183  	case "https", "http", "":
   184  		return s.dialWithHttpProxy(req, proxyURL)
   185  	}
   186  
   187  	return nil, fmt.Errorf("proxy URL scheme not supported: %s", proxyURL.Scheme)
   188  }
   189  
   190  // dialWithHttpProxy dials the host specified by url through an http or an https proxy.
   191  func (s *SpdyRoundTripper) dialWithHttpProxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) {
   192  	// ensure we use a canonical host with proxyReq
   193  	targetHost := netutil.CanonicalAddr(req.URL)
   194  
   195  	// proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support
   196  	proxyReq := http.Request{
   197  		Method: "CONNECT",
   198  		URL:    &url.URL{},
   199  		Host:   targetHost,
   200  	}
   201  
   202  	proxyReq = *proxyReq.WithContext(req.Context())
   203  
   204  	if pa := s.proxyAuth(proxyURL); pa != "" {
   205  		proxyReq.Header = http.Header{}
   206  		proxyReq.Header.Set("Proxy-Authorization", pa)
   207  	}
   208  
   209  	proxyDialConn, err := s.dialWithoutProxy(proxyReq.Context(), proxyURL)
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  
   214  	//nolint:staticcheck // SA1019 ignore deprecated httputil.NewProxyClientConn
   215  	proxyClientConn := httputil.NewProxyClientConn(proxyDialConn, nil)
   216  	response, err := proxyClientConn.Do(&proxyReq)
   217  	//nolint:staticcheck // SA1019 ignore deprecated httputil.ErrPersistEOF: it might be
   218  	// returned from the invocation of proxyClientConn.Do
   219  	if err != nil && err != httputil.ErrPersistEOF {
   220  		return nil, err
   221  	}
   222  	if response != nil && response.StatusCode >= 300 || response.StatusCode < 200 {
   223  		return nil, fmt.Errorf("CONNECT request to %s returned response: %s", proxyURL.Redacted(), response.Status)
   224  	}
   225  
   226  	rwc, _ := proxyClientConn.Hijack()
   227  
   228  	if req.URL.Scheme == "https" {
   229  		return s.tlsConn(proxyReq.Context(), rwc, targetHost)
   230  	}
   231  	return rwc, nil
   232  }
   233  
   234  // dialWithSocks5Proxy dials the host specified by url through a socks5 proxy.
   235  func (s *SpdyRoundTripper) dialWithSocks5Proxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) {
   236  	// ensure we use a canonical host with proxyReq
   237  	targetHost := netutil.CanonicalAddr(req.URL)
   238  	proxyDialAddr := netutil.CanonicalAddr(proxyURL)
   239  
   240  	var auth *proxy.Auth
   241  	if proxyURL.User != nil {
   242  		pass, _ := proxyURL.User.Password()
   243  		auth = &proxy.Auth{
   244  			User:     proxyURL.User.Username(),
   245  			Password: pass,
   246  		}
   247  	}
   248  
   249  	dialer := s.Dialer
   250  	if dialer == nil {
   251  		dialer = &net.Dialer{
   252  			Timeout: 30 * time.Second,
   253  		}
   254  	}
   255  
   256  	proxyDialer, err := proxy.SOCKS5("tcp", proxyDialAddr, auth, dialer)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	// According to the implementation of proxy.SOCKS5, the type assertion will always succeed
   262  	contextDialer, ok := proxyDialer.(proxy.ContextDialer)
   263  	if !ok {
   264  		return nil, errors.New("SOCKS5 Dialer must implement ContextDialer")
   265  	}
   266  
   267  	proxyDialConn, err := contextDialer.DialContext(req.Context(), "tcp", targetHost)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	if req.URL.Scheme == "https" {
   273  		return s.tlsConn(req.Context(), proxyDialConn, targetHost)
   274  	}
   275  	return proxyDialConn, nil
   276  }
   277  
   278  // tlsConn returns a TLS client side connection using rwc as the underlying transport.
   279  func (s *SpdyRoundTripper) tlsConn(ctx context.Context, rwc net.Conn, targetHost string) (net.Conn, error) {
   280  
   281  	host, _, err := net.SplitHostPort(targetHost)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	tlsConfig := s.tlsConfig
   287  	switch {
   288  	case tlsConfig == nil:
   289  		tlsConfig = &tls.Config{ServerName: host}
   290  	case len(tlsConfig.ServerName) == 0:
   291  		tlsConfig = tlsConfig.Clone()
   292  		tlsConfig.ServerName = host
   293  	}
   294  
   295  	tlsConn := tls.Client(rwc, tlsConfig)
   296  
   297  	if err := tlsConn.HandshakeContext(ctx); err != nil {
   298  		tlsConn.Close()
   299  		return nil, err
   300  	}
   301  
   302  	return tlsConn, nil
   303  }
   304  
   305  // dialWithoutProxy dials the host specified by url, using TLS if appropriate.
   306  func (s *SpdyRoundTripper) dialWithoutProxy(ctx context.Context, url *url.URL) (net.Conn, error) {
   307  	dialAddr := netutil.CanonicalAddr(url)
   308  	dialer := s.Dialer
   309  	if dialer == nil {
   310  		dialer = &net.Dialer{}
   311  	}
   312  
   313  	if url.Scheme == "http" {
   314  		return dialer.DialContext(ctx, "tcp", dialAddr)
   315  	}
   316  
   317  	tlsDialer := tls.Dialer{
   318  		NetDialer: dialer,
   319  		Config:    s.tlsConfig,
   320  	}
   321  	return tlsDialer.DialContext(ctx, "tcp", dialAddr)
   322  }
   323  
   324  // proxyAuth returns, for a given proxy URL, the value to be used for the Proxy-Authorization header
   325  func (s *SpdyRoundTripper) proxyAuth(proxyURL *url.URL) string {
   326  	if proxyURL == nil || proxyURL.User == nil {
   327  		return ""
   328  	}
   329  	username := proxyURL.User.Username()
   330  	password, _ := proxyURL.User.Password()
   331  	auth := username + ":" + password
   332  	return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
   333  }
   334  
   335  // RoundTrip executes the Request and upgrades it. After a successful upgrade,
   336  // clients may call SpdyRoundTripper.Connection() to retrieve the upgraded
   337  // connection.
   338  func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   339  	req = utilnet.CloneRequest(req)
   340  	req.Header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade)
   341  	req.Header.Add(httpstream.HeaderUpgrade, HeaderSpdy31)
   342  
   343  	conn, err := s.Dial(req)
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	responseReader := bufio.NewReader(conn)
   349  
   350  	resp, err := http.ReadResponse(responseReader, nil)
   351  	if err != nil {
   352  		conn.Close()
   353  		return nil, err
   354  	}
   355  
   356  	s.conn = conn
   357  
   358  	return resp, nil
   359  }
   360  
   361  // NewConnection validates the upgrade response, creating and returning a new
   362  // httpstream.Connection if there were no errors.
   363  func (s *SpdyRoundTripper) NewConnection(resp *http.Response) (httpstream.Connection, error) {
   364  	connectionHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderConnection))
   365  	upgradeHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderUpgrade))
   366  	if (resp.StatusCode != http.StatusSwitchingProtocols) || !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || !strings.Contains(upgradeHeader, strings.ToLower(HeaderSpdy31)) {
   367  		defer resp.Body.Close()
   368  		responseError := ""
   369  		responseErrorBytes, err := io.ReadAll(resp.Body)
   370  		if err != nil {
   371  			responseError = "unable to read error from server response"
   372  		} else {
   373  			// TODO: I don't belong here, I should be abstracted from this class
   374  			if obj, _, err := statusCodecs.UniversalDecoder().Decode(responseErrorBytes, nil, &metav1.Status{}); err == nil {
   375  				if status, ok := obj.(*metav1.Status); ok {
   376  					return nil, &apierrors.StatusError{ErrStatus: *status}
   377  				}
   378  			}
   379  			responseError = string(responseErrorBytes)
   380  			responseError = strings.TrimSpace(responseError)
   381  		}
   382  
   383  		return nil, fmt.Errorf("unable to upgrade connection: %s", responseError)
   384  	}
   385  
   386  	return NewClientConnectionWithPings(s.conn, s.pingPeriod)
   387  }
   388  
   389  // statusScheme is private scheme for the decoding here until someone fixes the TODO in NewConnection
   390  var statusScheme = runtime.NewScheme()
   391  
   392  // ParameterCodec knows about query parameters used with the meta v1 API spec.
   393  var statusCodecs = serializer.NewCodecFactory(statusScheme)
   394  
   395  func init() {
   396  	statusScheme.AddUnversionedTypes(metav1.SchemeGroupVersion,
   397  		&metav1.Status{},
   398  	)
   399  }
   400  

View as plain text