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 }