github.com/xeptore/docker-cli@v20.10.14+incompatible/cli/context/docker/load.go (about)

     1  package docker
     2  
     3  import (
     4  	"crypto/tls"
     5  	"crypto/x509"
     6  	"encoding/pem"
     7  	"fmt"
     8  	"net"
     9  	"net/http"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/docker/cli/cli/connhelper"
    14  	"github.com/docker/cli/cli/context"
    15  	"github.com/docker/cli/cli/context/store"
    16  	"github.com/docker/docker/client"
    17  	"github.com/docker/go-connections/tlsconfig"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  // EndpointMeta is a typed wrapper around a context-store generic endpoint describing
    22  // a Docker Engine endpoint, without its tls config
    23  type EndpointMeta = context.EndpointMetaBase
    24  
    25  // Endpoint is a typed wrapper around a context-store generic endpoint describing
    26  // a Docker Engine endpoint, with its tls data
    27  type Endpoint struct {
    28  	EndpointMeta
    29  	TLSData *context.TLSData
    30  
    31  	// Deprecated: Use of encrypted TLS private keys has been deprecated, and
    32  	// will be removed in a future release. Golang has deprecated support for
    33  	// legacy PEM encryption (as specified in RFC 1423), as it is insecure by
    34  	// design (see https://go-review.googlesource.com/c/go/+/264159).
    35  	TLSPassword string
    36  }
    37  
    38  // WithTLSData loads TLS materials for the endpoint
    39  func WithTLSData(s store.Reader, contextName string, m EndpointMeta) (Endpoint, error) {
    40  	tlsData, err := context.LoadTLSData(s, contextName, DockerEndpoint)
    41  	if err != nil {
    42  		return Endpoint{}, err
    43  	}
    44  	return Endpoint{
    45  		EndpointMeta: m,
    46  		TLSData:      tlsData,
    47  	}, nil
    48  }
    49  
    50  // tlsConfig extracts a context docker endpoint TLS config
    51  func (c *Endpoint) tlsConfig() (*tls.Config, error) {
    52  	if c.TLSData == nil && !c.SkipTLSVerify {
    53  		// there is no specific tls config
    54  		return nil, nil
    55  	}
    56  	var tlsOpts []func(*tls.Config)
    57  	if c.TLSData != nil && c.TLSData.CA != nil {
    58  		certPool := x509.NewCertPool()
    59  		if !certPool.AppendCertsFromPEM(c.TLSData.CA) {
    60  			return nil, errors.New("failed to retrieve context tls info: ca.pem seems invalid")
    61  		}
    62  		tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
    63  			cfg.RootCAs = certPool
    64  		})
    65  	}
    66  	if c.TLSData != nil && c.TLSData.Key != nil && c.TLSData.Cert != nil {
    67  		keyBytes := c.TLSData.Key
    68  		pemBlock, _ := pem.Decode(keyBytes)
    69  		if pemBlock == nil {
    70  			return nil, fmt.Errorf("no valid private key found")
    71  		}
    72  
    73  		var err error
    74  		// TODO should we follow Golang, and deprecate RFC 1423 encryption, and produce a warning (or just error)? see https://github.com/docker/cli/issues/3212
    75  		if x509.IsEncryptedPEMBlock(pemBlock) { //nolint: staticcheck // SA1019: x509.IsEncryptedPEMBlock is deprecated, and insecure by design
    76  			keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(c.TLSPassword)) //nolint: staticcheck // SA1019: x509.IsEncryptedPEMBlock is deprecated, and insecure by design
    77  			if err != nil {
    78  				return nil, errors.Wrap(err, "private key is encrypted, but could not decrypt it")
    79  			}
    80  			keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes})
    81  		}
    82  
    83  		x509cert, err := tls.X509KeyPair(c.TLSData.Cert, keyBytes)
    84  		if err != nil {
    85  			return nil, errors.Wrap(err, "failed to retrieve context tls info")
    86  		}
    87  		tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
    88  			cfg.Certificates = []tls.Certificate{x509cert}
    89  		})
    90  	}
    91  	if c.SkipTLSVerify {
    92  		tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
    93  			cfg.InsecureSkipVerify = true
    94  		})
    95  	}
    96  	return tlsconfig.ClientDefault(tlsOpts...), nil
    97  }
    98  
    99  // ClientOpts returns a slice of Client options to configure an API client with this endpoint
   100  func (c *Endpoint) ClientOpts() ([]client.Opt, error) {
   101  	var result []client.Opt
   102  	if c.Host != "" {
   103  		helper, err := connhelper.GetConnectionHelper(c.Host)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  		if helper == nil {
   108  			tlsConfig, err := c.tlsConfig()
   109  			if err != nil {
   110  				return nil, err
   111  			}
   112  			result = append(result,
   113  				withHTTPClient(tlsConfig),
   114  				client.WithHost(c.Host),
   115  			)
   116  
   117  		} else {
   118  			httpClient := &http.Client{
   119  				// No tls
   120  				// No proxy
   121  				Transport: &http.Transport{
   122  					DialContext: helper.Dialer,
   123  				},
   124  			}
   125  			result = append(result,
   126  				client.WithHTTPClient(httpClient),
   127  				client.WithHost(helper.Host),
   128  				client.WithDialContext(helper.Dialer),
   129  			)
   130  		}
   131  	}
   132  
   133  	version := os.Getenv("DOCKER_API_VERSION")
   134  	if version != "" {
   135  		result = append(result, client.WithVersion(version))
   136  	} else {
   137  		result = append(result, client.WithAPIVersionNegotiation())
   138  	}
   139  	return result, nil
   140  }
   141  
   142  func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error {
   143  	return func(c *client.Client) error {
   144  		if tlsConfig == nil {
   145  			// Use the default HTTPClient
   146  			return nil
   147  		}
   148  
   149  		httpClient := &http.Client{
   150  			Transport: &http.Transport{
   151  				TLSClientConfig: tlsConfig,
   152  				DialContext: (&net.Dialer{
   153  					KeepAlive: 30 * time.Second,
   154  					Timeout:   30 * time.Second,
   155  				}).DialContext,
   156  			},
   157  			CheckRedirect: client.CheckRedirect,
   158  		}
   159  		return client.WithHTTPClient(httpClient)(c)
   160  	}
   161  }
   162  
   163  // EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure
   164  func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) {
   165  	ep, ok := metadata.Endpoints[DockerEndpoint]
   166  	if !ok {
   167  		return EndpointMeta{}, errors.New("cannot find docker endpoint in context")
   168  	}
   169  	typed, ok := ep.(EndpointMeta)
   170  	if !ok {
   171  		return EndpointMeta{}, errors.Errorf("endpoint %q is not of type EndpointMeta", DockerEndpoint)
   172  	}
   173  	return typed, nil
   174  }