mirror of https://github.com/k3s-io/k3s
336 lines
9.9 KiB
Go
336 lines
9.9 KiB
Go
/*
|
|
Copyright 2015 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package spdy
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
|
"k8s.io/apimachinery/third_party/forked/golang/netutil"
|
|
)
|
|
|
|
// SpdyRoundTripper knows how to upgrade an HTTP request to one that supports
|
|
// multiplexed streams. After RoundTrip() is invoked, Conn will be set
|
|
// and usable. SpdyRoundTripper implements the UpgradeRoundTripper interface.
|
|
type SpdyRoundTripper struct {
|
|
//tlsConfig holds the TLS configuration settings to use when connecting
|
|
//to the remote server.
|
|
tlsConfig *tls.Config
|
|
|
|
/* TODO according to http://golang.org/pkg/net/http/#RoundTripper, a RoundTripper
|
|
must be safe for use by multiple concurrent goroutines. If this is absolutely
|
|
necessary, we could keep a map from http.Request to net.Conn. In practice,
|
|
a client will create an http.Client, set the transport to a new insteace of
|
|
SpdyRoundTripper, and use it a single time, so this hopefully won't be an issue.
|
|
*/
|
|
// conn is the underlying network connection to the remote server.
|
|
conn net.Conn
|
|
|
|
// Dialer is the dialer used to connect. Used if non-nil.
|
|
Dialer *net.Dialer
|
|
|
|
// proxier knows which proxy to use given a request, defaults to http.ProxyFromEnvironment
|
|
// Used primarily for mocking the proxy discovery in tests.
|
|
proxier func(req *http.Request) (*url.URL, error)
|
|
|
|
// followRedirects indicates if the round tripper should examine responses for redirects and
|
|
// follow them.
|
|
followRedirects bool
|
|
// requireSameHostRedirects restricts redirect following to only follow redirects to the same host
|
|
// as the original request.
|
|
requireSameHostRedirects bool
|
|
}
|
|
|
|
var _ utilnet.TLSClientConfigHolder = &SpdyRoundTripper{}
|
|
var _ httpstream.UpgradeRoundTripper = &SpdyRoundTripper{}
|
|
var _ utilnet.Dialer = &SpdyRoundTripper{}
|
|
|
|
// NewRoundTripper creates a new SpdyRoundTripper that will use
|
|
// the specified tlsConfig.
|
|
func NewRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) httpstream.UpgradeRoundTripper {
|
|
return NewSpdyRoundTripper(tlsConfig, followRedirects, requireSameHostRedirects)
|
|
}
|
|
|
|
// NewSpdyRoundTripper creates a new SpdyRoundTripper that will use
|
|
// the specified tlsConfig. This function is mostly meant for unit tests.
|
|
func NewSpdyRoundTripper(tlsConfig *tls.Config, followRedirects, requireSameHostRedirects bool) *SpdyRoundTripper {
|
|
return &SpdyRoundTripper{
|
|
tlsConfig: tlsConfig,
|
|
followRedirects: followRedirects,
|
|
requireSameHostRedirects: requireSameHostRedirects,
|
|
}
|
|
}
|
|
|
|
// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
|
|
// proxying with a spdy roundtripper.
|
|
func (s *SpdyRoundTripper) TLSClientConfig() *tls.Config {
|
|
return s.tlsConfig
|
|
}
|
|
|
|
// Dial implements k8s.io/apimachinery/pkg/util/net.Dialer.
|
|
func (s *SpdyRoundTripper) Dial(req *http.Request) (net.Conn, error) {
|
|
conn, err := s.dial(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := req.Write(conn); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// dial dials the host specified by req, using TLS if appropriate, optionally
|
|
// using a proxy server if one is configured via environment variables.
|
|
func (s *SpdyRoundTripper) dial(req *http.Request) (net.Conn, error) {
|
|
proxier := s.proxier
|
|
if proxier == nil {
|
|
proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
|
|
}
|
|
proxyURL, err := proxier(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if proxyURL == nil {
|
|
return s.dialWithoutProxy(req.Context(), req.URL)
|
|
}
|
|
|
|
// ensure we use a canonical host with proxyReq
|
|
targetHost := netutil.CanonicalAddr(req.URL)
|
|
|
|
// proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support
|
|
proxyReq := http.Request{
|
|
Method: "CONNECT",
|
|
URL: &url.URL{},
|
|
Host: targetHost,
|
|
}
|
|
|
|
if pa := s.proxyAuth(proxyURL); pa != "" {
|
|
proxyReq.Header = http.Header{}
|
|
proxyReq.Header.Set("Proxy-Authorization", pa)
|
|
}
|
|
|
|
proxyDialConn, err := s.dialWithoutProxy(req.Context(), proxyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proxyClientConn := httputil.NewProxyClientConn(proxyDialConn, nil)
|
|
_, err = proxyClientConn.Do(&proxyReq)
|
|
if err != nil && err != httputil.ErrPersistEOF {
|
|
return nil, err
|
|
}
|
|
|
|
rwc, _ := proxyClientConn.Hijack()
|
|
|
|
if req.URL.Scheme != "https" {
|
|
return rwc, nil
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(targetHost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConfig := s.tlsConfig
|
|
switch {
|
|
case tlsConfig == nil:
|
|
tlsConfig = &tls.Config{ServerName: host}
|
|
case len(tlsConfig.ServerName) == 0:
|
|
tlsConfig = tlsConfig.Clone()
|
|
tlsConfig.ServerName = host
|
|
}
|
|
|
|
tlsConn := tls.Client(rwc, tlsConfig)
|
|
|
|
// need to manually call Handshake() so we can call VerifyHostname() below
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return if we were configured to skip validation
|
|
if tlsConfig.InsecureSkipVerify {
|
|
return tlsConn, nil
|
|
}
|
|
|
|
if err := tlsConn.VerifyHostname(tlsConfig.ServerName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tlsConn, nil
|
|
}
|
|
|
|
// dialWithoutProxy dials the host specified by url, using TLS if appropriate.
|
|
func (s *SpdyRoundTripper) dialWithoutProxy(ctx context.Context, url *url.URL) (net.Conn, error) {
|
|
dialAddr := netutil.CanonicalAddr(url)
|
|
|
|
if url.Scheme == "http" {
|
|
if s.Dialer == nil {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, "tcp", dialAddr)
|
|
} else {
|
|
return s.Dialer.DialContext(ctx, "tcp", dialAddr)
|
|
}
|
|
}
|
|
|
|
// TODO validate the TLSClientConfig is set up?
|
|
var conn *tls.Conn
|
|
var err error
|
|
if s.Dialer == nil {
|
|
conn, err = tls.Dial("tcp", dialAddr, s.tlsConfig)
|
|
} else {
|
|
conn, err = tls.DialWithDialer(s.Dialer, "tcp", dialAddr, s.tlsConfig)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return if we were configured to skip validation
|
|
if s.tlsConfig != nil && s.tlsConfig.InsecureSkipVerify {
|
|
return conn, nil
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(dialAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.tlsConfig != nil && len(s.tlsConfig.ServerName) > 0 {
|
|
host = s.tlsConfig.ServerName
|
|
}
|
|
err = conn.VerifyHostname(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// proxyAuth returns, for a given proxy URL, the value to be used for the Proxy-Authorization header
|
|
func (s *SpdyRoundTripper) proxyAuth(proxyURL *url.URL) string {
|
|
if proxyURL == nil || proxyURL.User == nil {
|
|
return ""
|
|
}
|
|
credentials := proxyURL.User.String()
|
|
encodedAuth := base64.StdEncoding.EncodeToString([]byte(credentials))
|
|
return fmt.Sprintf("Basic %s", encodedAuth)
|
|
}
|
|
|
|
// RoundTrip executes the Request and upgrades it. After a successful upgrade,
|
|
// clients may call SpdyRoundTripper.Connection() to retrieve the upgraded
|
|
// connection.
|
|
func (s *SpdyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
header := utilnet.CloneHeader(req.Header)
|
|
header.Add(httpstream.HeaderConnection, httpstream.HeaderUpgrade)
|
|
header.Add(httpstream.HeaderUpgrade, HeaderSpdy31)
|
|
|
|
var (
|
|
conn net.Conn
|
|
rawResponse []byte
|
|
err error
|
|
)
|
|
|
|
if s.followRedirects {
|
|
conn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method, req.URL, header, req.Body, s, s.requireSameHostRedirects)
|
|
} else {
|
|
clone := utilnet.CloneRequest(req)
|
|
clone.Header = header
|
|
conn, err = s.Dial(clone)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
responseReader := bufio.NewReader(
|
|
io.MultiReader(
|
|
bytes.NewBuffer(rawResponse),
|
|
conn,
|
|
),
|
|
)
|
|
|
|
resp, err := http.ReadResponse(responseReader, nil)
|
|
if err != nil {
|
|
if conn != nil {
|
|
conn.Close()
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
s.conn = conn
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// NewConnection validates the upgrade response, creating and returning a new
|
|
// httpstream.Connection if there were no errors.
|
|
func (s *SpdyRoundTripper) NewConnection(resp *http.Response) (httpstream.Connection, error) {
|
|
connectionHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderConnection))
|
|
upgradeHeader := strings.ToLower(resp.Header.Get(httpstream.HeaderUpgrade))
|
|
if (resp.StatusCode != http.StatusSwitchingProtocols) || !strings.Contains(connectionHeader, strings.ToLower(httpstream.HeaderUpgrade)) || !strings.Contains(upgradeHeader, strings.ToLower(HeaderSpdy31)) {
|
|
defer resp.Body.Close()
|
|
responseError := ""
|
|
responseErrorBytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
responseError = "unable to read error from server response"
|
|
} else {
|
|
// TODO: I don't belong here, I should be abstracted from this class
|
|
if obj, _, err := statusCodecs.UniversalDecoder().Decode(responseErrorBytes, nil, &metav1.Status{}); err == nil {
|
|
if status, ok := obj.(*metav1.Status); ok {
|
|
return nil, &apierrors.StatusError{ErrStatus: *status}
|
|
}
|
|
}
|
|
responseError = string(responseErrorBytes)
|
|
responseError = strings.TrimSpace(responseError)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to upgrade connection: %s", responseError)
|
|
}
|
|
|
|
return NewClientConnection(s.conn)
|
|
}
|
|
|
|
// statusScheme is private scheme for the decoding here until someone fixes the TODO in NewConnection
|
|
var statusScheme = runtime.NewScheme()
|
|
|
|
// ParameterCodec knows about query parameters used with the meta v1 API spec.
|
|
var statusCodecs = serializer.NewCodecFactory(statusScheme)
|
|
|
|
func init() {
|
|
statusScheme.AddUnversionedTypes(metav1.SchemeGroupVersion,
|
|
&metav1.Status{},
|
|
)
|
|
}
|