2013-05-15 03:41:39 +02:00
|
|
|
package registry
|
|
|
|
|
|
|
|
import (
|
2013-12-04 15:03:51 +01:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2013-05-15 22:22:57 +02:00
|
|
|
"errors"
|
2013-05-15 03:41:39 +02:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2013-08-05 02:42:24 +02:00
|
|
|
"net"
|
2013-05-15 03:41:39 +02:00
|
|
|
"net/http"
|
2013-12-04 15:03:51 +01:00
|
|
|
"os"
|
|
|
|
"path"
|
2013-07-05 21:20:58 +02:00
|
|
|
"regexp"
|
2013-05-15 03:41:39 +02:00
|
|
|
"strings"
|
2013-08-05 02:42:24 +02:00
|
|
|
"time"
|
2014-04-29 11:01:07 +02:00
|
|
|
|
2014-10-11 05:22:12 +02:00
|
|
|
log "github.com/Sirupsen/logrus"
|
2014-07-25 00:19:50 +02:00
|
|
|
"github.com/docker/docker/utils"
|
2013-05-15 03:41:39 +02:00
|
|
|
)
|
|
|
|
|
2013-07-22 23:50:32 +02:00
|
|
|
var (
|
|
|
|
ErrAlreadyExists = errors.New("Image already exists")
|
|
|
|
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
|
2014-10-02 03:26:06 +02:00
|
|
|
ErrDoesNotExist = errors.New("Image does not exist")
|
2014-02-03 20:38:34 +01:00
|
|
|
errLoginRequired = errors.New("Authentication is required.")
|
2014-11-05 00:02:06 +01:00
|
|
|
validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`)
|
2014-09-16 05:30:10 +02:00
|
|
|
validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
|
2014-10-07 03:54:52 +02:00
|
|
|
emptyServiceConfig = NewServiceConfig(nil)
|
2013-07-22 23:50:32 +02:00
|
|
|
)
|
2013-05-15 22:22:57 +02:00
|
|
|
|
2013-12-04 15:03:51 +01:00
|
|
|
type TimeoutType uint32
|
|
|
|
|
|
|
|
const (
|
|
|
|
NoTimeout TimeoutType = iota
|
|
|
|
ReceiveTimeout
|
|
|
|
ConnectTimeout
|
|
|
|
)
|
|
|
|
|
2014-10-09 19:52:30 +02:00
|
|
|
func newClient(jar http.CookieJar, roots *x509.CertPool, certs []tls.Certificate, timeout TimeoutType, secure bool) *http.Client {
|
2014-10-16 04:39:51 +02:00
|
|
|
tlsConfig := tls.Config{
|
|
|
|
RootCAs: roots,
|
|
|
|
// Avoid fallback to SSL protocols < TLS1.0
|
2014-10-09 19:52:30 +02:00
|
|
|
MinVersion: tls.VersionTLS10,
|
|
|
|
Certificates: certs,
|
2013-12-04 15:03:51 +01:00
|
|
|
}
|
|
|
|
|
2014-10-11 05:22:12 +02:00
|
|
|
if !secure {
|
|
|
|
tlsConfig.InsecureSkipVerify = true
|
|
|
|
}
|
|
|
|
|
2013-12-04 15:03:51 +01:00
|
|
|
httpTransport := &http.Transport{
|
|
|
|
DisableKeepAlives: true,
|
|
|
|
Proxy: http.ProxyFromEnvironment,
|
|
|
|
TLSClientConfig: &tlsConfig,
|
|
|
|
}
|
|
|
|
|
|
|
|
switch timeout {
|
|
|
|
case ConnectTimeout:
|
|
|
|
httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
|
|
|
|
// Set the connect timeout to 5 seconds
|
2014-10-21 01:45:45 +02:00
|
|
|
d := net.Dialer{Timeout: 5 * time.Second, DualStack: true}
|
|
|
|
|
|
|
|
conn, err := d.Dial(proto, addr)
|
2013-12-04 15:03:51 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// Set the recv timeout to 10 seconds
|
|
|
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
case ReceiveTimeout:
|
|
|
|
httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
|
2014-10-21 01:45:45 +02:00
|
|
|
d := net.Dialer{DualStack: true}
|
|
|
|
|
|
|
|
conn, err := d.Dial(proto, addr)
|
2013-12-04 15:03:51 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
conn = utils.NewTimeoutConn(conn, 1*time.Minute)
|
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &http.Client{
|
|
|
|
Transport: httpTransport,
|
|
|
|
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
|
|
|
|
Jar: jar,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-11 05:22:12 +02:00
|
|
|
func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secure bool) (*http.Response, *http.Client, error) {
|
2013-12-04 15:03:51 +01:00
|
|
|
var (
|
|
|
|
pool *x509.CertPool
|
2014-10-09 19:52:30 +02:00
|
|
|
certs []tls.Certificate
|
2013-12-04 15:03:51 +01:00
|
|
|
)
|
|
|
|
|
2014-10-11 05:22:12 +02:00
|
|
|
if secure && req.URL.Scheme == "https" {
|
|
|
|
hasFile := func(files []os.FileInfo, name string) bool {
|
|
|
|
for _, f := range files {
|
|
|
|
if f.Name() == name {
|
|
|
|
return true
|
|
|
|
}
|
2013-12-04 15:03:51 +01:00
|
|
|
}
|
2014-10-11 05:22:12 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
|
|
|
|
log.Debugf("hostDir: %s", hostDir)
|
|
|
|
fs, err := ioutil.ReadDir(hostDir)
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return nil, nil, err
|
2013-12-04 15:03:51 +01:00
|
|
|
}
|
2014-10-11 05:22:12 +02:00
|
|
|
|
|
|
|
for _, f := range fs {
|
|
|
|
if strings.HasSuffix(f.Name(), ".crt") {
|
|
|
|
if pool == nil {
|
|
|
|
pool = x509.NewCertPool()
|
|
|
|
}
|
|
|
|
log.Debugf("crt: %s", hostDir+"/"+f.Name())
|
|
|
|
data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
pool.AppendCertsFromPEM(data)
|
2013-12-04 15:03:51 +01:00
|
|
|
}
|
2014-10-11 05:22:12 +02:00
|
|
|
if strings.HasSuffix(f.Name(), ".cert") {
|
|
|
|
certName := f.Name()
|
|
|
|
keyName := certName[:len(certName)-5] + ".key"
|
|
|
|
log.Debugf("cert: %s", hostDir+"/"+f.Name())
|
|
|
|
if !hasFile(fs, keyName) {
|
|
|
|
return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
|
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2014-10-09 19:52:30 +02:00
|
|
|
certs = append(certs, cert)
|
2014-08-25 18:50:18 +02:00
|
|
|
}
|
2014-10-11 05:22:12 +02:00
|
|
|
if strings.HasSuffix(f.Name(), ".key") {
|
|
|
|
keyName := f.Name()
|
|
|
|
certName := keyName[:len(keyName)-4] + ".cert"
|
|
|
|
log.Debugf("key: %s", hostDir+"/"+f.Name())
|
|
|
|
if !hasFile(fs, certName) {
|
|
|
|
return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
|
|
|
}
|
2013-12-04 15:03:51 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(certs) == 0 {
|
2014-10-11 05:22:12 +02:00
|
|
|
client := newClient(jar, pool, nil, timeout, secure)
|
2013-12-04 15:03:51 +01:00
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return res, client, nil
|
2014-08-25 18:50:18 +02:00
|
|
|
}
|
2014-10-11 05:22:12 +02:00
|
|
|
|
2014-10-09 19:52:30 +02:00
|
|
|
client := newClient(jar, pool, certs, timeout, secure)
|
|
|
|
res, err := client.Do(req)
|
|
|
|
return res, client, err
|
2013-12-04 15:03:51 +01:00
|
|
|
}
|
|
|
|
|
2014-10-07 03:54:52 +02:00
|
|
|
func validateRemoteName(remoteName string) error {
|
2013-07-05 23:30:43 +02:00
|
|
|
var (
|
|
|
|
namespace string
|
|
|
|
name string
|
|
|
|
)
|
2014-10-07 03:54:52 +02:00
|
|
|
nameParts := strings.SplitN(remoteName, "/", 2)
|
2013-07-05 23:30:43 +02:00
|
|
|
if len(nameParts) < 2 {
|
|
|
|
namespace = "library"
|
|
|
|
name = nameParts[0]
|
2014-08-18 02:50:15 +02:00
|
|
|
|
2014-11-27 22:55:03 +01:00
|
|
|
// the repository name must not be a valid image ID
|
|
|
|
if err := utils.ValidateID(name); err == nil {
|
2014-08-18 02:50:15 +02:00
|
|
|
return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
|
|
|
|
}
|
2013-07-05 23:30:43 +02:00
|
|
|
} else {
|
|
|
|
namespace = nameParts[0]
|
|
|
|
name = nameParts[1]
|
|
|
|
}
|
2014-11-05 00:02:06 +01:00
|
|
|
if !validNamespaceChars.MatchString(namespace) {
|
|
|
|
return fmt.Errorf("Invalid namespace name (%s). Only [a-z0-9-_] are allowed.", namespace)
|
|
|
|
}
|
|
|
|
if len(namespace) < 4 || len(namespace) > 30 {
|
|
|
|
return fmt.Errorf("Invalid namespace name (%s). Cannot be fewer than 4 or more than 30 characters.", namespace)
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(namespace, "-") || strings.HasSuffix(namespace, "-") {
|
|
|
|
return fmt.Errorf("Invalid namespace name (%s). Cannot begin or end with a hyphen.", namespace)
|
|
|
|
}
|
|
|
|
if strings.Contains(namespace, "--") {
|
|
|
|
return fmt.Errorf("Invalid namespace name (%s). Cannot contain consecutive hyphens.", namespace)
|
2013-07-05 21:20:58 +02:00
|
|
|
}
|
|
|
|
if !validRepo.MatchString(name) {
|
2013-09-25 17:33:09 +02:00
|
|
|
return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name)
|
2013-07-05 21:20:58 +02:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2014-10-07 03:54:52 +02:00
|
|
|
// NewIndexInfo returns IndexInfo configuration from indexName
|
|
|
|
func NewIndexInfo(config *ServiceConfig, indexName string) (*IndexInfo, error) {
|
|
|
|
var err error
|
|
|
|
indexName, err = ValidateIndexName(indexName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return any configured index info, first.
|
|
|
|
if index, ok := config.IndexConfigs[indexName]; ok {
|
|
|
|
return index, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct a non-configured index info.
|
|
|
|
index := &IndexInfo{
|
|
|
|
Name: indexName,
|
|
|
|
Mirrors: make([]string, 0),
|
|
|
|
Official: false,
|
|
|
|
}
|
|
|
|
index.Secure = config.isSecureIndex(indexName)
|
|
|
|
return index, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateNoSchema(reposName string) error {
|
2013-07-09 20:30:12 +02:00
|
|
|
if strings.Contains(reposName, "://") {
|
|
|
|
// It cannot contain a scheme!
|
2014-10-07 03:54:52 +02:00
|
|
|
return ErrInvalidRepositoryName
|
2013-07-09 20:30:12 +02:00
|
|
|
}
|
2014-10-07 03:54:52 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// splitReposName breaks a reposName into an index name and remote name
|
|
|
|
func splitReposName(reposName string) (string, string) {
|
2013-07-05 21:20:58 +02:00
|
|
|
nameParts := strings.SplitN(reposName, "/", 2)
|
2014-10-07 03:54:52 +02:00
|
|
|
var indexName, remoteName string
|
|
|
|
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
|
|
|
|
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
2013-07-05 21:20:58 +02:00
|
|
|
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
2014-10-07 03:54:52 +02:00
|
|
|
// 'docker.io'
|
|
|
|
indexName = IndexServerName()
|
|
|
|
remoteName = reposName
|
|
|
|
} else {
|
|
|
|
indexName = nameParts[0]
|
|
|
|
remoteName = nameParts[1]
|
|
|
|
}
|
|
|
|
return indexName, remoteName
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
|
|
|
|
func NewRepositoryInfo(config *ServiceConfig, reposName string) (*RepositoryInfo, error) {
|
|
|
|
if err := validateNoSchema(reposName); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
indexName, remoteName := splitReposName(reposName)
|
|
|
|
if err := validateRemoteName(remoteName); err != nil {
|
|
|
|
return nil, err
|
2013-07-05 21:20:58 +02:00
|
|
|
}
|
2014-10-07 03:54:52 +02:00
|
|
|
|
|
|
|
repoInfo := &RepositoryInfo{
|
|
|
|
RemoteName: remoteName,
|
2013-07-10 01:46:55 +02:00
|
|
|
}
|
2014-10-07 03:54:52 +02:00
|
|
|
|
|
|
|
var err error
|
|
|
|
repoInfo.Index, err = NewIndexInfo(config, indexName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2013-07-10 01:46:55 +02:00
|
|
|
}
|
2014-02-20 23:57:58 +01:00
|
|
|
|
2014-10-07 03:54:52 +02:00
|
|
|
if repoInfo.Index.Official {
|
|
|
|
normalizedName := repoInfo.RemoteName
|
|
|
|
if strings.HasPrefix(normalizedName, "library/") {
|
|
|
|
// If pull "library/foo", it's stored locally under "foo"
|
|
|
|
normalizedName = strings.SplitN(normalizedName, "/", 2)[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
repoInfo.LocalName = normalizedName
|
|
|
|
repoInfo.RemoteName = normalizedName
|
|
|
|
// If the normalized name does not contain a '/' (e.g. "foo")
|
|
|
|
// then it is an official repo.
|
|
|
|
if strings.IndexRune(normalizedName, '/') == -1 {
|
|
|
|
repoInfo.Official = true
|
|
|
|
// Fix up remote name for official repos.
|
|
|
|
repoInfo.RemoteName = "library/" + normalizedName
|
|
|
|
}
|
|
|
|
|
|
|
|
// *TODO: Prefix this with 'docker.io/'.
|
|
|
|
repoInfo.CanonicalName = repoInfo.LocalName
|
|
|
|
} else {
|
|
|
|
// *TODO: Decouple index name from hostname (via registry configuration?)
|
|
|
|
repoInfo.LocalName = repoInfo.Index.Name + "/" + repoInfo.RemoteName
|
|
|
|
repoInfo.CanonicalName = repoInfo.LocalName
|
|
|
|
}
|
|
|
|
return repoInfo, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ValidateRepositoryName validates a repository name
|
|
|
|
func ValidateRepositoryName(reposName string) error {
|
|
|
|
var err error
|
|
|
|
if err = validateNoSchema(reposName); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
indexName, remoteName := splitReposName(reposName)
|
|
|
|
if _, err = ValidateIndexName(indexName); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return validateRemoteName(remoteName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but
|
|
|
|
// lacks registry configuration.
|
|
|
|
func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) {
|
|
|
|
return NewRepositoryInfo(emptyServiceConfig, reposName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NormalizeLocalName transforms a repository name into a normalize LocalName
|
|
|
|
// Passes through the name without transformation on error (image id, etc)
|
|
|
|
func NormalizeLocalName(name string) string {
|
|
|
|
repoInfo, err := ParseRepositoryInfo(name)
|
|
|
|
if err != nil {
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
return repoInfo.LocalName
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAuthConfigKey special-cases using the full index address of the official
|
|
|
|
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
|
|
|
func (index *IndexInfo) GetAuthConfigKey() string {
|
|
|
|
if index.Official {
|
|
|
|
return IndexServerAddress()
|
|
|
|
}
|
|
|
|
return index.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetSearchTerm special-cases using local name for official index, and
|
|
|
|
// remote name for private indexes.
|
|
|
|
func (repoInfo *RepositoryInfo) GetSearchTerm() string {
|
|
|
|
if repoInfo.Index.Official {
|
|
|
|
return repoInfo.LocalName
|
|
|
|
}
|
|
|
|
return repoInfo.RemoteName
|
2013-09-03 20:45:49 +02:00
|
|
|
}
|
|
|
|
|
2014-06-05 20:37:37 +02:00
|
|
|
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 {
|
2014-06-07 23:17:56 +02:00
|
|
|
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
2014-06-05 20:37:37 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2014-03-26 01:33:17 +01:00
|
|
|
func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
|
|
|
if via != nil && via[0] != nil {
|
2014-06-05 20:37:37 +02:00
|
|
|
if trustedLocation(req) && trustedLocation(via[0]) {
|
|
|
|
req.Header = via[0].Header
|
2014-08-25 18:50:18 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
for k, v := range via[0].Header {
|
|
|
|
if k != "Authorization" {
|
|
|
|
for _, vv := range v {
|
|
|
|
req.Header.Add(k, vv)
|
2014-06-05 20:37:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-03-26 01:33:17 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|