github.com/stchris/docker@v1.4.2-0.20150106053530-1510a324dbd5/registry/registry.go (about)

     1  package registry
     2  
     3  import (
     4  	"crypto/tls"
     5  	"crypto/x509"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	log "github.com/Sirupsen/logrus"
    18  	"github.com/docker/docker/utils"
    19  )
    20  
    21  var (
    22  	ErrAlreadyExists         = errors.New("Image already exists")
    23  	ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
    24  	ErrDoesNotExist          = errors.New("Image does not exist")
    25  	errLoginRequired         = errors.New("Authentication is required.")
    26  	validNamespaceChars      = regexp.MustCompile(`^([a-z0-9-_]*)$`)
    27  	validRepo                = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
    28  )
    29  
    30  type TimeoutType uint32
    31  
    32  const (
    33  	NoTimeout TimeoutType = iota
    34  	ReceiveTimeout
    35  	ConnectTimeout
    36  )
    37  
    38  func newClient(jar http.CookieJar, roots *x509.CertPool, certs []tls.Certificate, timeout TimeoutType, secure bool) *http.Client {
    39  	tlsConfig := tls.Config{
    40  		RootCAs: roots,
    41  		// Avoid fallback to SSL protocols < TLS1.0
    42  		MinVersion:   tls.VersionTLS10,
    43  		Certificates: certs,
    44  	}
    45  
    46  	if !secure {
    47  		tlsConfig.InsecureSkipVerify = true
    48  	}
    49  
    50  	httpTransport := &http.Transport{
    51  		DisableKeepAlives: true,
    52  		Proxy:             http.ProxyFromEnvironment,
    53  		TLSClientConfig:   &tlsConfig,
    54  	}
    55  
    56  	switch timeout {
    57  	case ConnectTimeout:
    58  		httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
    59  			// Set the connect timeout to 5 seconds
    60  			d := net.Dialer{Timeout: 5 * time.Second, DualStack: true}
    61  
    62  			conn, err := d.Dial(proto, addr)
    63  			if err != nil {
    64  				return nil, err
    65  			}
    66  			// Set the recv timeout to 10 seconds
    67  			conn.SetDeadline(time.Now().Add(10 * time.Second))
    68  			return conn, nil
    69  		}
    70  	case ReceiveTimeout:
    71  		httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
    72  			d := net.Dialer{DualStack: true}
    73  
    74  			conn, err := d.Dial(proto, addr)
    75  			if err != nil {
    76  				return nil, err
    77  			}
    78  			conn = utils.NewTimeoutConn(conn, 1*time.Minute)
    79  			return conn, nil
    80  		}
    81  	}
    82  
    83  	return &http.Client{
    84  		Transport:     httpTransport,
    85  		CheckRedirect: AddRequiredHeadersToRedirectedRequests,
    86  		Jar:           jar,
    87  	}
    88  }
    89  
    90  func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secure bool) (*http.Response, *http.Client, error) {
    91  	var (
    92  		pool  *x509.CertPool
    93  		certs []tls.Certificate
    94  	)
    95  
    96  	if secure && req.URL.Scheme == "https" {
    97  		hasFile := func(files []os.FileInfo, name string) bool {
    98  			for _, f := range files {
    99  				if f.Name() == name {
   100  					return true
   101  				}
   102  			}
   103  			return false
   104  		}
   105  
   106  		hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
   107  		log.Debugf("hostDir: %s", hostDir)
   108  		fs, err := ioutil.ReadDir(hostDir)
   109  		if err != nil && !os.IsNotExist(err) {
   110  			return nil, nil, err
   111  		}
   112  
   113  		for _, f := range fs {
   114  			if strings.HasSuffix(f.Name(), ".crt") {
   115  				if pool == nil {
   116  					pool = x509.NewCertPool()
   117  				}
   118  				log.Debugf("crt: %s", hostDir+"/"+f.Name())
   119  				data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
   120  				if err != nil {
   121  					return nil, nil, err
   122  				}
   123  				pool.AppendCertsFromPEM(data)
   124  			}
   125  			if strings.HasSuffix(f.Name(), ".cert") {
   126  				certName := f.Name()
   127  				keyName := certName[:len(certName)-5] + ".key"
   128  				log.Debugf("cert: %s", hostDir+"/"+f.Name())
   129  				if !hasFile(fs, keyName) {
   130  					return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
   131  				}
   132  				cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName))
   133  				if err != nil {
   134  					return nil, nil, err
   135  				}
   136  				certs = append(certs, cert)
   137  			}
   138  			if strings.HasSuffix(f.Name(), ".key") {
   139  				keyName := f.Name()
   140  				certName := keyName[:len(keyName)-4] + ".cert"
   141  				log.Debugf("key: %s", hostDir+"/"+f.Name())
   142  				if !hasFile(fs, certName) {
   143  					return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
   144  				}
   145  			}
   146  		}
   147  	}
   148  
   149  	if len(certs) == 0 {
   150  		client := newClient(jar, pool, nil, timeout, secure)
   151  		res, err := client.Do(req)
   152  		if err != nil {
   153  			return nil, nil, err
   154  		}
   155  		return res, client, nil
   156  	}
   157  
   158  	client := newClient(jar, pool, certs, timeout, secure)
   159  	res, err := client.Do(req)
   160  	return res, client, err
   161  }
   162  
   163  func validateRepositoryName(repositoryName string) error {
   164  	var (
   165  		namespace string
   166  		name      string
   167  	)
   168  	nameParts := strings.SplitN(repositoryName, "/", 2)
   169  	if len(nameParts) < 2 {
   170  		namespace = "library"
   171  		name = nameParts[0]
   172  
   173  		// the repository name must not be a valid image ID
   174  		if err := utils.ValidateID(name); err == nil {
   175  			return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
   176  		}
   177  	} else {
   178  		namespace = nameParts[0]
   179  		name = nameParts[1]
   180  	}
   181  	if !validNamespaceChars.MatchString(namespace) {
   182  		return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace)
   183  	}
   184  	if len(namespace) < 4 || len(namespace) > 30 {
   185  		return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace)
   186  	}
   187  	if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") {
   188  		return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace)
   189  	}
   190  	if strings.Contains(namespace, "--") {
   191  		return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace)
   192  	}
   193  	if !validRepo.MatchString(name) {
   194  		return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name)
   195  	}
   196  	return nil
   197  }
   198  
   199  // Resolves a repository name to a hostname + name
   200  func ResolveRepositoryName(reposName string) (string, string, error) {
   201  	if strings.Contains(reposName, "://") {
   202  		// It cannot contain a scheme!
   203  		return "", "", ErrInvalidRepositoryName
   204  	}
   205  	nameParts := strings.SplitN(reposName, "/", 2)
   206  	if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") &&
   207  		nameParts[0] != "localhost") {
   208  		// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
   209  		err := validateRepositoryName(reposName)
   210  		return IndexServerAddress(), reposName, err
   211  	}
   212  	hostname := nameParts[0]
   213  	reposName = nameParts[1]
   214  	if strings.Contains(hostname, "index.docker.io") {
   215  		return "", "", fmt.Errorf("Invalid repository name, try \"%s\" instead", reposName)
   216  	}
   217  	if err := validateRepositoryName(reposName); err != nil {
   218  		return "", "", err
   219  	}
   220  
   221  	return hostname, reposName, nil
   222  }
   223  
   224  func trustedLocation(req *http.Request) bool {
   225  	var (
   226  		trusteds = []string{"docker.com", "docker.io"}
   227  		hostname = strings.SplitN(req.Host, ":", 2)[0]
   228  	)
   229  	if req.URL.Scheme != "https" {
   230  		return false
   231  	}
   232  
   233  	for _, trusted := range trusteds {
   234  		if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
   235  			return true
   236  		}
   237  	}
   238  	return false
   239  }
   240  
   241  func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
   242  	if via != nil && via[0] != nil {
   243  		if trustedLocation(req) && trustedLocation(via[0]) {
   244  			req.Header = via[0].Header
   245  			return nil
   246  		}
   247  		for k, v := range via[0].Header {
   248  			if k != "Authorization" {
   249  				for _, vv := range v {
   250  					req.Header.Add(k, vv)
   251  				}
   252  			}
   253  		}
   254  	}
   255  	return nil
   256  }