03778bd1d2
ContinueOnError assumes that something of type errcode.Errors contains at least one error. This is generally true, but might not be true if the remote registry returns an empty error body or invalid JSON. Add the bounds check, and in the case where it fails, allow fallbacks to v1. Fixes #18481 Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
255 lines
7.4 KiB
Go
255 lines
7.4 KiB
Go
// 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/tlsconfig"
|
|
"github.com/docker/docker/pkg/useragent"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func shouldV2Fallback(err errcode.Error) bool {
|
|
logrus.Debugf("v2 error: %T %v", err, err)
|
|
switch err.Code {
|
|
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
|
|
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,
|
|
}
|
|
}
|