github.com/spotmaxtech/k8s-apimachinery-v0260@v0.0.1/pkg/util/httpstream/spdy/roundtripper.go (about)

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