// Package registry contains client primitives to interact with a remote Docker registry. package registry import ( "crypto/tls" "crypto/x509" "errors" "fmt" "io/ioutil" "net" "net/http" "os" "path/filepath" "runtime" "strings" "syscall" "time" "github.com/Sirupsen/logrus" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/client" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/parsers/kernel" "github.com/docker/docker/pkg/useragent" "github.com/docker/go-connections/tlsconfig" ) var ( // ErrAlreadyExists is an error returned if an image being pushed // already exists on the remote side ErrAlreadyExists = errors.New("Image already exists") errLoginRequired = errors.New("Authentication is required.") ) // dockerUserAgent is the User-Agent the Docker client uses to identify itself. // It is populated on init(), comprising version information of different components. var dockerUserAgent string func init() { httpVersion := make([]useragent.VersionInfo, 0, 6) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: dockerversion.Version}) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()}) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: dockerversion.GitCommit}) if kernelVersion, err := kernel.GetKernelVersion(); err == nil { httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()}) } httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS}) httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH}) dockerUserAgent = useragent.AppendVersions("", httpVersion...) if runtime.GOOS != "linux" { V2Only = true } } func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { // PreferredServerCipherSuites should have no effect tlsConfig := tlsconfig.ServerDefault tlsConfig.InsecureSkipVerify = !isSecure if isSecure && CertsDir != "" { hostDir := filepath.Join(CertsDir, cleanPath(hostname)) logrus.Debugf("hostDir: %s", hostDir) if err := ReadCertsDirectory(&tlsConfig, hostDir); err != nil { return nil, err } } return &tlsConfig, nil } func hasFile(files []os.FileInfo, name string) bool { for _, f := range files { if f.Name() == name { return true } } return false } // ReadCertsDirectory reads the directory for TLS certificates // including roots and certificate pairs and updates the // provided TLS configuration. func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { fs, err := ioutil.ReadDir(directory) if err != nil && !os.IsNotExist(err) { return err } for _, f := range fs { if strings.HasSuffix(f.Name(), ".crt") { if tlsConfig.RootCAs == nil { // TODO(dmcgowan): Copy system pool tlsConfig.RootCAs = x509.NewCertPool() } logrus.Debugf("crt: %s", filepath.Join(directory, f.Name())) data, err := ioutil.ReadFile(filepath.Join(directory, f.Name())) if err != nil { return err } tlsConfig.RootCAs.AppendCertsFromPEM(data) } if strings.HasSuffix(f.Name(), ".cert") { certName := f.Name() keyName := certName[:len(certName)-5] + ".key" logrus.Debugf("cert: %s", filepath.Join(directory, f.Name())) if !hasFile(fs, keyName) { return fmt.Errorf("Missing key %s for certificate %s", keyName, certName) } cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) if err != nil { return err } tlsConfig.Certificates = append(tlsConfig.Certificates, cert) } if strings.HasSuffix(f.Name(), ".key") { keyName := f.Name() certName := keyName[:len(keyName)-4] + ".cert" logrus.Debugf("key: %s", filepath.Join(directory, f.Name())) if !hasFile(fs, certName) { return fmt.Errorf("Missing certificate %s for key %s", certName, keyName) } } } return nil } // DockerHeaders returns request modifiers that ensure requests have // the User-Agent header set to dockerUserAgent and that metaHeaders // are added. func DockerHeaders(metaHeaders http.Header) []transport.RequestModifier { modifiers := []transport.RequestModifier{ transport.NewHeaderRequestModifier(http.Header{"User-Agent": []string{dockerUserAgent}}), } if metaHeaders != nil { modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) } return modifiers } // HTTPClient returns a HTTP client structure which uses the given transport // and contains the necessary headers for redirected requests func HTTPClient(transport http.RoundTripper) *http.Client { return &http.Client{ Transport: transport, CheckRedirect: addRequiredHeadersToRedirectedRequests, } } func trustedLocation(req *http.Request) bool { var ( trusteds = []string{"docker.com", "docker.io"} hostname = strings.SplitN(req.Host, ":", 2)[0] ) if req.URL.Scheme != "https" { return false } for _, trusted := range trusteds { if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) { return true } } return false } // addRequiredHeadersToRedirectedRequests adds the necessary redirection headers // for redirected requests func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error { if via != nil && via[0] != nil { if trustedLocation(req) && trustedLocation(via[0]) { req.Header = via[0].Header return nil } for k, v := range via[0].Header { if k != "Authorization" { for _, vv := range v { req.Header.Add(k, vv) } } } } return nil } // ShouldV2Fallback returns true if this error is a reason to fall back to v1. func ShouldV2Fallback(err errcode.Error) bool { switch err.Code { case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown: return true } return false } // ErrNoSupport is an error type used for errors indicating that an operation // is not supported. It encapsulates a more specific error. type ErrNoSupport struct{ Err error } func (e ErrNoSupport) Error() string { if e.Err == nil { return "not supported" } return e.Err.Error() } // ContinueOnError returns true if we should fallback to the next endpoint // as a result of this error. func ContinueOnError(err error) bool { switch v := err.(type) { case errcode.Errors: if len(v) == 0 { return true } return ContinueOnError(v[0]) case ErrNoSupport: return ContinueOnError(v.Err) case errcode.Error: return ShouldV2Fallback(v) case *client.UnexpectedHTTPResponseError: return true case error: return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error())) } // let's be nice and fallback if the error is a completely // unexpected one. // If new errors have to be handled in some way, please // add them to the switch above. return true } // NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the // default TLS configuration. func NewTransport(tlsConfig *tls.Config) *http.Transport { if tlsConfig == nil { var cfg = tlsconfig.ServerDefault tlsConfig = &cfg } return &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).Dial, TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: tlsConfig, // TODO(dmcgowan): Call close idle connections when complete and use keep alive DisableKeepAlives: true, } }