Commit d072232d authored by Clayton Coleman's avatar Clayton Coleman

Allow client.Config to be used for HTTP2 and WebSocket connections

client.Config describes how to make a client connection to a server for HTTP traffic, but for connection upgrade scenarios cannot be used because the underlying http.Transport object can't allow the connection to be hijacked. Reorganize the TLS and connection wrapper methods so that a sophisticated client can do: cfg := &client.Config{...} // from somewhere tlsConfig, _ := client.TLSConfigFor(cfg) _ := conn.Dial(...) rt := MyRoundTripper() // some func that implements grabbing requests wrapper, _ := client.HTTPWrappersFor(cfg) req := &http.Request{} req.Header.Set("Connection-Upgrade", ...) _, := wrapper.RoundTrip(req) // rt has been invoked with a fully formed Req with auth rt.Req.Write(conn) // read response for upgrade It would be good to have utility function that does more of this, but mostly enabling the HTTP2/SPDY client exec function right now.
parent 7603f889
...@@ -61,22 +61,8 @@ type Config struct { ...@@ -61,22 +61,8 @@ type Config struct {
// TODO: demonstrate an OAuth2 compatible client. // TODO: demonstrate an OAuth2 compatible client.
BearerToken string BearerToken string
// Server requires TLS client certificate authentication // TLSClientConfig contains settings to enable transport layer security
CertFile string TLSClientConfig
// Server requires TLS client certificate authentication
KeyFile string
// Trusted root certificates for server
CAFile string
// CertData holds PEM-encoded bytes (typically read from a client certificate file).
// CertData takes precedence over CertFile
CertData []byte
// KeyData holds PEM-encoded bytes (typically read from a client certificate key file).
// KeyData takes precedence over KeyFile
KeyData []byte
// CAData holds PEM-encoded bytes (typically read from a root certificates bundle).
// CAData takes precedence over CAFile
CAData []byte
// Server should be accessed without verifying the TLS // Server should be accessed without verifying the TLS
// certificate. For testing only. // certificate. For testing only.
...@@ -92,11 +78,17 @@ type KubeletConfig struct { ...@@ -92,11 +78,17 @@ type KubeletConfig struct {
Port uint Port uint
EnableHttps bool EnableHttps bool
// TLS Configuration, only applies if EnableHttps is true. // TLSClientConfig contains settings to enable transport layer security
TLSClientConfig
}
// TLSClientConfig contains settings to enable transport layer security
type TLSClientConfig struct {
// Server requires TLS client certificate authentication
CertFile string CertFile string
// TLS Configuration, only applies if EnableHttps is true. // Server requires TLS client certificate authentication
KeyFile string KeyFile string
// TLS Configuration, only applies if EnableHttps is true. // Trusted root certificates for server
CAFile string CAFile string
// CertData holds PEM-encoded bytes (typically read from a client certificate file). // CertData holds PEM-encoded bytes (typically read from a client certificate file).
...@@ -215,47 +207,40 @@ func TransportFor(config *Config) (http.RoundTripper, error) { ...@@ -215,47 +207,40 @@ func TransportFor(config *Config) (http.RoundTripper, error) {
if config.Transport != nil && (hasCA || hasCert || config.Insecure) { if config.Transport != nil && (hasCA || hasCert || config.Insecure) {
return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed") return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed")
} }
if hasCA && config.Insecure {
return nil, fmt.Errorf("specifying a root certificates file with the insecure flag is not allowed") tlsConfig, err := TLSConfigFor(config)
if err != nil {
return nil, err
} }
var transport http.RoundTripper var transport http.RoundTripper
switch { if config.Transport != nil {
case config.Transport != nil:
transport = config.Transport transport = config.Transport
case hasCert: } else {
var ( if tlsConfig != nil {
certData, keyData, caData []byte transport = &http.Transport{
err error TLSClientConfig: tlsConfig,
) }
if certData, err = dataFromSliceOrFile(config.CertData, config.CertFile); err != nil { } else {
return nil, err transport = http.DefaultTransport
}
if keyData, err = dataFromSliceOrFile(config.KeyData, config.KeyFile); err != nil {
return nil, err
} }
if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil {
return nil, err
}
if transport, err = NewClientCertTLSTransport(certData, keyData, caData); err != nil {
return nil, err
}
case hasCA:
var (
caData []byte
err error
)
if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil {
return nil, err
}
if transport, err = NewTLSTransport(caData); err != nil {
return nil, err
}
case config.Insecure:
transport = NewUnsafeTLSTransport()
default:
transport = http.DefaultTransport
} }
transport, err = HTTPWrappersForConfig(config, transport)
if err != nil {
return nil, err
}
// TODO: use the config context to wrap a transport
return transport, nil
}
// HTTPWrappersForConfig wraps a round tripper with any relevant layered behavior from the
// config. Exposed to allow more clients that need HTTP-like behavior but then must hijack
// the underlying connection (like WebSocket or HTTP2 clients). Pure HTTP clients should use
// the higher level TransportFor or RESTClientFor methods.
func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTripper, error) {
// Set authentication wrappers // Set authentication wrappers
hasBasicAuth := config.Username != "" || config.Password != "" hasBasicAuth := config.Username != "" || config.Password != ""
if hasBasicAuth && config.BearerToken != "" { if hasBasicAuth && config.BearerToken != "" {
...@@ -263,14 +248,11 @@ func TransportFor(config *Config) (http.RoundTripper, error) { ...@@ -263,14 +248,11 @@ func TransportFor(config *Config) (http.RoundTripper, error) {
} }
switch { switch {
case config.BearerToken != "": case config.BearerToken != "":
transport = NewBearerAuthRoundTripper(config.BearerToken, transport) rt = NewBearerAuthRoundTripper(config.BearerToken, rt)
case hasBasicAuth: case hasBasicAuth:
transport = NewBasicAuthRoundTripper(config.Username, config.Password, transport) rt = NewBasicAuthRoundTripper(config.Username, config.Password, rt)
} }
return rt, nil
// TODO: use the config context to wrap a transport
return transport, nil
} }
// dataFromSliceOrFile returns data from the slice (if non-empty), or from the file, // dataFromSliceOrFile returns data from the slice (if non-empty), or from the file,
......
...@@ -104,54 +104,68 @@ func TestTransportFor(t *testing.T) { ...@@ -104,54 +104,68 @@ func TestTransportFor(t *testing.T) {
"ca transport": { "ca transport": {
TLS: true, TLS: true,
Config: &Config{ Config: &Config{
CAData: []byte(rootCACert), TLSClientConfig: TLSClientConfig{
CAData: []byte(rootCACert),
},
}, },
}, },
"bad ca file transport": { "bad ca file transport": {
Err: true, Err: true,
Config: &Config{ Config: &Config{
CAFile: "invalid file", TLSClientConfig: TLSClientConfig{
CAFile: "invalid file",
},
}, },
}, },
"ca data overriding bad ca file transport": { "ca data overriding bad ca file transport": {
TLS: true, TLS: true,
Config: &Config{ Config: &Config{
CAData: []byte(rootCACert), TLSClientConfig: TLSClientConfig{
CAFile: "invalid file", CAData: []byte(rootCACert),
CAFile: "invalid file",
},
}, },
}, },
"cert transport": { "cert transport": {
TLS: true, TLS: true,
Config: &Config{ Config: &Config{
CertData: []byte(certData), TLSClientConfig: TLSClientConfig{
KeyData: []byte(keyData), CertData: []byte(certData),
CAData: []byte(rootCACert), KeyData: []byte(keyData),
CAData: []byte(rootCACert),
},
}, },
}, },
"bad cert data transport": { "bad cert data transport": {
Err: true, Err: true,
Config: &Config{ Config: &Config{
CertData: []byte(certData), TLSClientConfig: TLSClientConfig{
KeyData: []byte("bad key data"), CertData: []byte(certData),
CAData: []byte(rootCACert), KeyData: []byte("bad key data"),
CAData: []byte(rootCACert),
},
}, },
}, },
"bad file cert transport": { "bad file cert transport": {
Err: true, Err: true,
Config: &Config{ Config: &Config{
CertData: []byte(certData), TLSClientConfig: TLSClientConfig{
KeyFile: "invalid file", CertData: []byte(certData),
CAData: []byte(rootCACert), KeyFile: "invalid file",
CAData: []byte(rootCACert),
},
}, },
}, },
"key data overriding bad file cert transport": { "key data overriding bad file cert transport": {
TLS: true, TLS: true,
Config: &Config{ Config: &Config{
CertData: []byte(certData), TLSClientConfig: TLSClientConfig{
KeyData: []byte(keyData), CertData: []byte(certData),
KeyFile: "invalid file", KeyData: []byte(keyData),
CAData: []byte(rootCACert), KeyFile: "invalid file",
CAData: []byte(rootCACert),
},
}, },
}, },
} }
...@@ -206,15 +220,19 @@ func TestIsConfigTransportTLS(t *testing.T) { ...@@ -206,15 +220,19 @@ func TestIsConfigTransportTLS(t *testing.T) {
}, },
{ {
Config: &Config{ Config: &Config{
Host: "localhost", Host: "localhost",
CertFile: "foo", TLSClientConfig: TLSClientConfig{
CertFile: "foo",
},
}, },
TransportTLS: true, TransportTLS: true,
}, },
{ {
Config: &Config{ Config: &Config{
Host: "///:://localhost", Host: "///:://localhost",
CertFile: "foo", TLSClientConfig: TLSClientConfig{
CertFile: "foo",
},
}, },
TransportTLS: false, TransportTLS: false,
}, },
......
...@@ -59,41 +59,25 @@ type HTTPKubeletClient struct { ...@@ -59,41 +59,25 @@ type HTTPKubeletClient struct {
EnableHttps bool EnableHttps bool
} }
// TODO: this structure is questionable, it should be using client.Config and overriding defaults.
func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) { func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) {
transport := http.DefaultTransport transport := http.DefaultTransport
hasCA := len(config.CAFile) > 0 || len(config.CAData) > 0
hasCert := len(config.CertFile) > 0 || len(config.CertData) > 0 tlsConfig, err := TLSConfigFor(&Config{
if hasCert { TLSClientConfig: config.TLSClientConfig,
var ( })
certData, keyData, caData []byte if err != nil {
err error return nil, err
) }
if certData, err = dataFromSliceOrFile(config.CertData, config.CertFile); err != nil { if tlsConfig != nil {
return nil, err transport = &http.Transport{
} TLSClientConfig: tlsConfig,
if keyData, err = dataFromSliceOrFile(config.KeyData, config.KeyFile); err != nil {
return nil, err
}
if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil {
return nil, err
}
if transport, err = NewClientCertTLSTransport(certData, keyData, caData); err != nil {
return nil, err
}
} else if hasCA {
var (
caData []byte
err error
)
if caData, err = dataFromSliceOrFile(config.CAData, config.CAFile); err != nil {
return nil, err
}
if transport, err = NewTLSTransport(caData); err != nil {
return nil, err
} }
} }
c := &http.Client{Transport: transport} c := &http.Client{
Transport: transport,
}
return &HTTPKubeletClient{ return &HTTPKubeletClient{
Client: c, Client: c,
Port: config.Port, Port: config.Port,
......
...@@ -147,9 +147,11 @@ func TestNewKubeletClientTLSInvalid(t *testing.T) { ...@@ -147,9 +147,11 @@ func TestNewKubeletClientTLSInvalid(t *testing.T) {
Port: 9000, Port: 9000,
EnableHttps: true, EnableHttps: true,
//Invalid certificate and key path //Invalid certificate and key path
CertFile: "./testdata/mycertinvalid.cer", TLSClientConfig: TLSClientConfig{
KeyFile: "./testdata/mycertinvalid.key", CertFile: "./testdata/mycertinvalid.cer",
CAFile: "./testdata/myCA.cer", KeyFile: "./testdata/mycertinvalid.key",
CAFile: "./testdata/myCA.cer",
},
} }
client, err := NewKubeletClient(config) client, err := NewKubeletClient(config)
...@@ -165,11 +167,13 @@ func TestNewKubeletClientTLSValid(t *testing.T) { ...@@ -165,11 +167,13 @@ func TestNewKubeletClientTLSValid(t *testing.T) {
config := &KubeletConfig{ config := &KubeletConfig{
Port: 9000, Port: 9000,
EnableHttps: true, EnableHttps: true,
CertFile: "./testdata/mycertvalid.cer", TLSClientConfig: TLSClientConfig{
// TLS Configuration, only applies if EnableHttps is true. CertFile: "./testdata/mycertvalid.cer",
KeyFile: "./testdata/mycertvalid.key", // TLS Configuration, only applies if EnableHttps is true.
// TLS Configuration, only applies if EnableHttps is true. KeyFile: "./testdata/mycertvalid.key",
CAFile: "./testdata/myCA.cer", // TLS Configuration, only applies if EnableHttps is true.
CAFile: "./testdata/myCA.cer",
},
} }
client, err := NewKubeletClient(config) client, err := NewKubeletClient(config)
......
...@@ -54,44 +54,84 @@ func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, ...@@ -54,44 +54,84 @@ func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response,
return rt.rt.RoundTrip(req) return rt.rt.RoundTrip(req)
} }
func NewClientCertTLSTransport(certData, keyData, caData []byte) (*http.Transport, error) { // TLSConfigFor returns a tls.Config that will provide the transport level security defined
// by the provided Config. Will return nil if no transport level security is requested.
func TLSConfigFor(config *Config) (*tls.Config, error) {
hasCA := len(config.CAFile) > 0 || len(config.CAData) > 0
hasCert := len(config.CertFile) > 0 || len(config.CertData) > 0
if hasCA && config.Insecure {
return nil, fmt.Errorf("specifying a root certificates file with the insecure flag is not allowed")
}
var tlsConfig *tls.Config
switch {
case hasCert:
certData, err := dataFromSliceOrFile(config.CertData, config.CertFile)
if err != nil {
return nil, err
}
keyData, err := dataFromSliceOrFile(config.KeyData, config.KeyFile)
if err != nil {
return nil, err
}
caData, err := dataFromSliceOrFile(config.CAData, config.CAFile)
if err != nil {
return nil, err
}
cfg, err := NewClientCertTLSConfig(certData, keyData, caData)
if err != nil {
return nil, err
}
tlsConfig = cfg
case hasCA:
caData, err := dataFromSliceOrFile(config.CAData, config.CAFile)
if err != nil {
return nil, err
}
cfg, err := NewTLSConfig(caData)
if err != nil {
return nil, err
}
tlsConfig = cfg
case config.Insecure:
tlsConfig = NewUnsafeTLSConfig()
}
return tlsConfig, nil
}
func NewClientCertTLSConfig(certData, keyData, caData []byte) (*tls.Config, error) {
cert, err := tls.X509KeyPair(certData, keyData) cert, err := tls.X509KeyPair(certData, keyData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
certPool := x509.NewCertPool() certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caData) certPool.AppendCertsFromPEM(caData)
return &http.Transport{ return &tls.Config{
TLSClientConfig: &tls.Config{ // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability) MinVersion: tls.VersionTLS10,
MinVersion: tls.VersionTLS10, Certificates: []tls.Certificate{
Certificates: []tls.Certificate{ cert,
cert,
},
RootCAs: certPool,
ClientCAs: certPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}, },
RootCAs: certPool,
ClientCAs: certPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}, nil }, nil
} }
func NewTLSTransport(caData []byte) (*http.Transport, error) { func NewTLSConfig(caData []byte) (*tls.Config, error) {
certPool := x509.NewCertPool() certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caData) certPool.AppendCertsFromPEM(caData)
return &http.Transport{ return &tls.Config{
TLSClientConfig: &tls.Config{ // Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability) MinVersion: tls.VersionTLS10,
MinVersion: tls.VersionTLS10, RootCAs: certPool,
RootCAs: certPool,
},
}, nil }, nil
} }
func NewUnsafeTLSTransport() *http.Transport { func NewUnsafeTLSConfig() *tls.Config {
return &http.Transport{ return &tls.Config{
TLSClientConfig: &tls.Config{ InsecureSkipVerify: true,
InsecureSkipVerify: true,
},
} }
} }
......
...@@ -23,9 +23,9 @@ import ( ...@@ -23,9 +23,9 @@ import (
) )
func TestUnsecuredTLSTransport(t *testing.T) { func TestUnsecuredTLSTransport(t *testing.T) {
transport := NewUnsafeTLSTransport() cfg := NewUnsafeTLSConfig()
if !transport.TLSClientConfig.InsecureSkipVerify { if !cfg.InsecureSkipVerify {
t.Errorf("expected transport to be insecure") t.Errorf("expected config to be insecure")
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment