ebaa771c3b
Add a daemon flag to control this behaviour. Add a warning message when pulling an image from a v1 registry. The default order of pull is slightly altered with this changset. Previously it was: https v2, https v1, http v2, http v1 now it is: https v2, http v2, https v1, http v1 Prevent login to v1 registries by explicitly setting the version before ping to prevent fallback to v1. Add unit tests for v2 only mode. Create a mock server that can register handlers for various endpoints. Assert no v1 endpoints are hit with legacy registries disabled for the following commands: pull, push, build, run and login. Assert the opposite when legacy registries are not disabled. Signed-off-by: Richard Scothern <richard.scothern@gmail.com>
250 lines
7.2 KiB
Go
250 lines
7.2 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"
|
|
"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/autogen/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{"docker", dockerversion.VERSION})
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{"go", runtime.Version()})
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{"git-commit", dockerversion.GITCOMMIT})
|
|
if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{"kernel", kernelVersion.String()})
|
|
}
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{"os", runtime.GOOS})
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{"arch", 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 {
|
|
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 v2.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:
|
|
return ContinueOnError(v[0])
|
|
case ErrNoSupport:
|
|
return ContinueOnError(v.Err)
|
|
case errcode.Error:
|
|
return shouldV2Fallback(v)
|
|
case *client.UnexpectedHTTPResponseError:
|
|
return true
|
|
}
|
|
// 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,
|
|
}
|
|
}
|