k3s/pkg/clientaccess/token.go
Brad Davidson 360b0f1ee5 Add timeout to clientaccess http client
The default http client does not have an overall request timeout, so
connections to misbehaving or unavailable servers can stall for an
excessive amount of time. At the moment, just attempting to join
an unavailable cluster takes 2 minutes and 40 seconds to timeout.

Resolve that by setting a reasonable request timeout.

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
2020-09-27 03:26:27 -07:00

278 lines
7.0 KiB
Go

package clientaccess
import (
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
)
var (
defaultClientTimeout = 20 * time.Second
defaultClient = &http.Client{
Timeout: defaultClientTimeout,
}
insecureClient = &http.Client{
Timeout: defaultClientTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
)
const (
tokenPrefix = "K10"
tokenFormat = "%s%s::%s:%s"
)
type OverrideURLCallback func(config []byte) (*url.URL, error)
type Info struct {
CACerts []byte `json:"cacerts,omitempty"`
BaseURL string `json:"baseurl,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
caHash string
}
// String returns the token data, templated according to the token format
func (info *Info) String() string {
return fmt.Sprintf(tokenFormat, tokenPrefix, hashCA(info.CACerts), info.Username, info.Password)
}
// ParseAndValidateToken parses a token, downloads and validates the server's CA bundle,
// and validates it according to the caHash from the token if set.
func ParseAndValidateToken(server string, token string) (*Info, error) {
info, err := parseToken(token)
if err != nil {
return nil, err
}
if err := info.setServer(server); err != nil {
return nil, err
}
if info.caHash != "" {
if err := info.validateCAHash(); err != nil {
return nil, err
}
}
return info, nil
}
// ParseAndValidateToken parses a token with user override, downloads and
// validates the server's CA bundle, and validates it according to the caHash from the token if set.
func ParseAndValidateTokenForUser(server string, token string, username string) (*Info, error) {
info, err := parseToken(token)
if err != nil {
return nil, err
}
info.Username = username
if err := info.setServer(server); err != nil {
return nil, err
}
if info.caHash != "" {
if err := info.validateCAHash(); err != nil {
return nil, err
}
}
return info, nil
}
// validateCACerts returns a boolean indicating whether or not a CA bundle matches the provided hash,
// and a string containing the hash of the CA bundle.
func validateCACerts(cacerts []byte, hash string) (bool, string) {
if len(cacerts) == 0 && hash == "" {
return true, ""
}
newHash := hashCA(cacerts)
return hash == newHash, newHash
}
// hashCA returns the hex-encoded SHA256 digest of a byte array.
func hashCA(cacerts []byte) string {
digest := sha256.Sum256(cacerts)
return hex.EncodeToString(digest[:])
}
// ParseUsernamePassword returns the username and password portion of a token string,
// along with a bool indicating if the token was successfully parsed.
func ParseUsernamePassword(token string) (string, string, bool) {
info, err := parseToken(token)
if err != nil {
return "", "", false
}
return info.Username, info.Password, true
}
// parseToken parses a token into an Info struct
func parseToken(token string) (*Info, error) {
var info = &Info{}
if !strings.HasPrefix(token, tokenPrefix) {
token = fmt.Sprintf(tokenFormat, tokenPrefix, "", "", token)
}
// Strip off the prefix
token = token[len(tokenPrefix):]
parts := strings.SplitN(token, "::", 2)
token = parts[0]
if len(parts) > 1 {
info.caHash = parts[0]
token = parts[1]
}
parts = strings.SplitN(token, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid token format")
}
info.Username = parts[0]
info.Password = parts[1]
return info, nil
}
// GetHTTPClient returns a http client that validates TLS server certificates using the provided CA bundle.
// If the CA bundle is empty, it validates using the default http client using the OS CA bundle.
// If the CA bundle is not empty but does not contain any valid certs, it validates using
// an empty CA bundle (which will always fail).
func GetHTTPClient(cacerts []byte) *http.Client {
if len(cacerts) == 0 {
return defaultClient
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(cacerts)
return &http.Client{
Timeout: defaultClientTimeout,
Transport: &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
RootCAs: pool,
},
},
}
}
// Get makes a request to a subpath of info's BaseURL
func Get(path string, info *Info) ([]byte, error) {
u, err := url.Parse(info.BaseURL)
if err != nil {
return nil, err
}
u.Path = path
return get(u.String(), GetHTTPClient(info.CACerts), info.Username, info.Password)
}
// setServer sets the BaseURL and CACerts fields of the Info by connecting to the server
// and storing the CA bundle.
func (info *Info) setServer(server string) error {
url, err := url.Parse(server)
if err != nil {
return errors.Wrapf(err, "Invalid server url, failed to parse: %s", server)
}
if url.Scheme != "https" {
return fmt.Errorf("only https:// URLs are supported, invalid scheme: %s", server)
}
for strings.HasSuffix(url.Path, "/") {
url.Path = url.Path[:len(url.Path)-1]
}
cacerts, err := getCACerts(*url)
if err != nil {
return err
}
info.BaseURL = url.String()
info.CACerts = cacerts
return nil
}
// ValidateCAHash validates that info's caHash matches the CACerts hash.
func (info *Info) validateCAHash() error {
if ok, serverHash := validateCACerts(info.CACerts, info.caHash); !ok {
return fmt.Errorf("token CA hash does not match the server CA hash: %s != %s", info.caHash, serverHash)
}
return nil
}
// getCACerts retrieves the CA bundle from a server.
// An error is raised if the CA bundle cannot be retrieved,
// or if the server's cert is not signed by the returned bundle.
func getCACerts(u url.URL) ([]byte, error) {
u.Path = "/cacerts"
url := u.String()
// This first request is expected to fail. If the server has
// a cert that can be validated using the default CA bundle, return
// success with no CA certs.
_, err := get(url, defaultClient, "", "")
if err == nil {
return nil, nil
}
// Download the CA bundle using a client that does not validate certs.
cacerts, err := get(url, insecureClient, "", "")
if err != nil {
return nil, errors.Wrap(err, "failed to get CA certs")
}
// Request the CA bundle again, validating that the CA bundle can be loaded
// and used to validate the server certificate. This should only fail if we somehow
// get an empty CA bundle. or if the dynamiclistener cert is incorrectly signed.
_, err = get(url, GetHTTPClient(cacerts), "", "")
if err != nil {
return nil, errors.Wrap(err, "CA cert validation failed")
}
return cacerts, nil
}
// get makes a request to a url using a provided client, username, and password,
// returning the response body.
func get(u string, client *http.Client, username, password string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, err
}
if username != "" {
req.SetBasicAuth(username, password)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: %s", u, resp.Status)
}
return ioutil.ReadAll(resp.Body)
}