github.com/mheon/docker@v0.11.2-0.20150922122814-44f47903a831/registry/registry.go (about)

     1  // Package registry contains client primitives to interact with a remote Docker registry.
     2  package registry
     3  
     4  import (
     5  	"crypto/tls"
     6  	"crypto/x509"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/Sirupsen/logrus"
    19  	"github.com/docker/distribution/registry/api/errcode"
    20  	"github.com/docker/distribution/registry/api/v2"
    21  	"github.com/docker/distribution/registry/client"
    22  	"github.com/docker/distribution/registry/client/transport"
    23  	"github.com/docker/docker/autogen/dockerversion"
    24  	"github.com/docker/docker/pkg/parsers/kernel"
    25  	"github.com/docker/docker/pkg/tlsconfig"
    26  	"github.com/docker/docker/pkg/useragent"
    27  )
    28  
    29  var (
    30  	// ErrAlreadyExists is an error returned if an image being pushed
    31  	// already exists on the remote side
    32  	ErrAlreadyExists = errors.New("Image already exists")
    33  	errLoginRequired = errors.New("Authentication is required.")
    34  )
    35  
    36  // dockerUserAgent is the User-Agent the Docker client uses to identify itself.
    37  // It is populated on init(), comprising version information of different components.
    38  var dockerUserAgent string
    39  
    40  func init() {
    41  	httpVersion := make([]useragent.VersionInfo, 0, 6)
    42  	httpVersion = append(httpVersion, useragent.VersionInfo{"docker", dockerversion.VERSION})
    43  	httpVersion = append(httpVersion, useragent.VersionInfo{"go", runtime.Version()})
    44  	httpVersion = append(httpVersion, useragent.VersionInfo{"git-commit", dockerversion.GITCOMMIT})
    45  	if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
    46  		httpVersion = append(httpVersion, useragent.VersionInfo{"kernel", kernelVersion.String()})
    47  	}
    48  	httpVersion = append(httpVersion, useragent.VersionInfo{"os", runtime.GOOS})
    49  	httpVersion = append(httpVersion, useragent.VersionInfo{"arch", runtime.GOARCH})
    50  
    51  	dockerUserAgent = useragent.AppendVersions("", httpVersion...)
    52  }
    53  
    54  func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {
    55  	// PreferredServerCipherSuites should have no effect
    56  	tlsConfig := tlsconfig.ServerDefault
    57  
    58  	tlsConfig.InsecureSkipVerify = !isSecure
    59  
    60  	if isSecure {
    61  		hostDir := filepath.Join(CertsDir, cleanPath(hostname))
    62  		logrus.Debugf("hostDir: %s", hostDir)
    63  		if err := ReadCertsDirectory(&tlsConfig, hostDir); err != nil {
    64  			return nil, err
    65  		}
    66  	}
    67  
    68  	return &tlsConfig, nil
    69  }
    70  
    71  func hasFile(files []os.FileInfo, name string) bool {
    72  	for _, f := range files {
    73  		if f.Name() == name {
    74  			return true
    75  		}
    76  	}
    77  	return false
    78  }
    79  
    80  // ReadCertsDirectory reads the directory for TLS certificates
    81  // including roots and certificate pairs and updates the
    82  // provided TLS configuration.
    83  func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
    84  	fs, err := ioutil.ReadDir(directory)
    85  	if err != nil && !os.IsNotExist(err) {
    86  		return err
    87  	}
    88  
    89  	for _, f := range fs {
    90  		if strings.HasSuffix(f.Name(), ".crt") {
    91  			if tlsConfig.RootCAs == nil {
    92  				// TODO(dmcgowan): Copy system pool
    93  				tlsConfig.RootCAs = x509.NewCertPool()
    94  			}
    95  			logrus.Debugf("crt: %s", filepath.Join(directory, f.Name()))
    96  			data, err := ioutil.ReadFile(filepath.Join(directory, f.Name()))
    97  			if err != nil {
    98  				return err
    99  			}
   100  			tlsConfig.RootCAs.AppendCertsFromPEM(data)
   101  		}
   102  		if strings.HasSuffix(f.Name(), ".cert") {
   103  			certName := f.Name()
   104  			keyName := certName[:len(certName)-5] + ".key"
   105  			logrus.Debugf("cert: %s", filepath.Join(directory, f.Name()))
   106  			if !hasFile(fs, keyName) {
   107  				return fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
   108  			}
   109  			cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
   110  			if err != nil {
   111  				return err
   112  			}
   113  			tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
   114  		}
   115  		if strings.HasSuffix(f.Name(), ".key") {
   116  			keyName := f.Name()
   117  			certName := keyName[:len(keyName)-4] + ".cert"
   118  			logrus.Debugf("key: %s", filepath.Join(directory, f.Name()))
   119  			if !hasFile(fs, certName) {
   120  				return fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
   121  			}
   122  		}
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  // DockerHeaders returns request modifiers that ensure requests have
   129  // the User-Agent header set to dockerUserAgent and that metaHeaders
   130  // are added.
   131  func DockerHeaders(metaHeaders http.Header) []transport.RequestModifier {
   132  	modifiers := []transport.RequestModifier{
   133  		transport.NewHeaderRequestModifier(http.Header{"User-Agent": []string{dockerUserAgent}}),
   134  	}
   135  	if metaHeaders != nil {
   136  		modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
   137  	}
   138  	return modifiers
   139  }
   140  
   141  // HTTPClient returns a HTTP client structure which uses the given transport
   142  // and contains the necessary headers for redirected requests
   143  func HTTPClient(transport http.RoundTripper) *http.Client {
   144  	return &http.Client{
   145  		Transport:     transport,
   146  		CheckRedirect: addRequiredHeadersToRedirectedRequests,
   147  	}
   148  }
   149  
   150  func trustedLocation(req *http.Request) bool {
   151  	var (
   152  		trusteds = []string{"docker.com", "docker.io"}
   153  		hostname = strings.SplitN(req.Host, ":", 2)[0]
   154  	)
   155  	if req.URL.Scheme != "https" {
   156  		return false
   157  	}
   158  
   159  	for _, trusted := range trusteds {
   160  		if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
   161  			return true
   162  		}
   163  	}
   164  	return false
   165  }
   166  
   167  // addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
   168  // for redirected requests
   169  func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
   170  	if via != nil && via[0] != nil {
   171  		if trustedLocation(req) && trustedLocation(via[0]) {
   172  			req.Header = via[0].Header
   173  			return nil
   174  		}
   175  		for k, v := range via[0].Header {
   176  			if k != "Authorization" {
   177  				for _, vv := range v {
   178  					req.Header.Add(k, vv)
   179  				}
   180  			}
   181  		}
   182  	}
   183  	return nil
   184  }
   185  
   186  func shouldV2Fallback(err errcode.Error) bool {
   187  	logrus.Debugf("v2 error: %T %v", err, err)
   188  	switch err.Code {
   189  	case v2.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
   190  		return true
   191  	}
   192  	return false
   193  }
   194  
   195  // ErrNoSupport is an error type used for errors indicating that an operation
   196  // is not supported. It encapsulates a more specific error.
   197  type ErrNoSupport struct{ Err error }
   198  
   199  func (e ErrNoSupport) Error() string {
   200  	if e.Err == nil {
   201  		return "not supported"
   202  	}
   203  	return e.Err.Error()
   204  }
   205  
   206  // ContinueOnError returns true if we should fallback to the next endpoint
   207  // as a result of this error.
   208  func ContinueOnError(err error) bool {
   209  	switch v := err.(type) {
   210  	case errcode.Errors:
   211  		return ContinueOnError(v[0])
   212  	case ErrNoSupport:
   213  		return ContinueOnError(v.Err)
   214  	case errcode.Error:
   215  		return shouldV2Fallback(v)
   216  	case *client.UnexpectedHTTPResponseError:
   217  		return true
   218  	}
   219  	// let's be nice and fallback if the error is a completely
   220  	// unexpected one.
   221  	// If new errors have to be handled in some way, please
   222  	// add them to the switch above.
   223  	return true
   224  }
   225  
   226  // NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
   227  // default TLS configuration.
   228  func NewTransport(tlsConfig *tls.Config) *http.Transport {
   229  	if tlsConfig == nil {
   230  		var cfg = tlsconfig.ServerDefault
   231  		tlsConfig = &cfg
   232  	}
   233  	return &http.Transport{
   234  		Proxy: http.ProxyFromEnvironment,
   235  		Dial: (&net.Dialer{
   236  			Timeout:   30 * time.Second,
   237  			KeepAlive: 30 * time.Second,
   238  			DualStack: true,
   239  		}).Dial,
   240  		TLSHandshakeTimeout: 10 * time.Second,
   241  		TLSClientConfig:     tlsConfig,
   242  		// TODO(dmcgowan): Call close idle connections when complete and use keep alive
   243  		DisableKeepAlives: true,
   244  	}
   245  }