diff --git a/docs/auth.go b/docs/auth.go index 8382869b..102078d7 100644 --- a/docs/auth.go +++ b/docs/auth.go @@ -17,13 +17,6 @@ import ( const ( // Where we store the config file CONFIGFILE = ".dockercfg" - - // Only used for user auth + account creation - INDEXSERVER = "https://index.docker.io/v1/" - REGISTRYSERVER = "https://registry-1.docker.io/v1/" - INDEXNAME = "docker.io" - - // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" ) var ( @@ -43,14 +36,6 @@ type ConfigFile struct { rootPath string } -func IndexServerAddress() string { - return INDEXSERVER -} - -func IndexServerName() string { - return INDEXNAME -} - // create a base64 encoded auth string to store in config func encodeAuth(authConfig *AuthConfig) string { authStr := authConfig.Username + ":" + authConfig.Password diff --git a/docs/config.go b/docs/config.go index bd993edd..b5652b15 100644 --- a/docs/config.go +++ b/docs/config.go @@ -2,12 +2,16 @@ package registry import ( "encoding/json" + "errors" "fmt" "net" "net/url" + "regexp" + "strings" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" ) // Options holds command line options. @@ -16,6 +20,30 @@ type Options struct { InsecureRegistries opts.ListOpts } +const ( + // Only used for user auth + account creation + INDEXSERVER = "https://index.docker.io/v1/" + REGISTRYSERVER = "https://registry-1.docker.io/v1/" + INDEXNAME = "docker.io" + + // INDEXSERVER = "https://registry-stage.hub.docker.com/v1/" +) + +var ( + ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") + emptyServiceConfig = NewServiceConfig(nil) + validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) + validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) +) + +func IndexServerAddress() string { + return INDEXSERVER +} + +func IndexServerName() string { + return INDEXNAME +} + // InstallFlags adds command-line options to the top-level flag parser for // the current process. func (options *Options) InstallFlags() { @@ -25,34 +53,6 @@ func (options *Options) InstallFlags() { flag.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)") } -// ValidateMirror validates an HTTP(S) registry mirror -func ValidateMirror(val string) (string, error) { - uri, err := url.Parse(val) - if err != nil { - return "", fmt.Errorf("%s is not a valid URI", val) - } - - if uri.Scheme != "http" && uri.Scheme != "https" { - return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) - } - - if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { - return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") - } - - return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil -} - -// ValidateIndexName validates an index name. -func ValidateIndexName(val string) (string, error) { - // 'index.docker.io' => 'docker.io' - if val == "index."+IndexServerName() { - val = IndexServerName() - } - // *TODO: Check if valid hostname[:port]/ip[:port]? - return val, nil -} - type netIPNet net.IPNet func (ipnet *netIPNet) MarshalJSON() ([]byte, error) { @@ -124,3 +124,259 @@ func NewServiceConfig(options *Options) *ServiceConfig { return config } + +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered +// insecure. +// +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element +// of insecureRegistries. +func (config *ServiceConfig) isSecureIndex(indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides NewIndexInfo, in order to honor per-index configurations. + if index, ok := config.IndexConfigs[indexName]; ok { + return index.Secure + } + + host, _, err := net.SplitHostPort(indexName) + if err != nil { + // assume indexName is of the form `host` without the port and go on. + host = indexName + } + + addrs, err := lookupIP(host) + if err != nil { + ip := net.ParseIP(host) + if ip != nil { + addrs = []net.IP{ip} + } + + // if ip == nil, then `host` is neither an IP nor it could be looked up, + // either because the index is unreachable, or because the index is behind an HTTP proxy. + // So, len(addrs) == 0 and we're not aborting. + } + + // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. + for _, addr := range addrs { + for _, ipnet := range config.InsecureRegistryCIDRs { + // check if the addr falls in the subnet + if (*net.IPNet)(ipnet).Contains(addr) { + return false + } + } + } + + return true +} + +// ValidateMirror validates an HTTP(S) registry mirror +func ValidateMirror(val string) (string, error) { + uri, err := url.Parse(val) + if err != nil { + return "", fmt.Errorf("%s is not a valid URI", val) + } + + if uri.Scheme != "http" && uri.Scheme != "https" { + return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) + } + + if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { + return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") + } + + return fmt.Sprintf("%s://%s/v1/", uri.Scheme, uri.Host), nil +} + +// ValidateIndexName validates an index name. +func ValidateIndexName(val string) (string, error) { + // 'index.docker.io' => 'docker.io' + if val == "index."+IndexServerName() { + val = IndexServerName() + } + // *TODO: Check if valid hostname[:port]/ip[:port]? + return val, nil +} + +func validateRemoteName(remoteName string) error { + var ( + namespace string + name string + ) + nameParts := strings.SplitN(remoteName, "/", 2) + if len(nameParts) < 2 { + namespace = "library" + name = nameParts[0] + + // the repository name must not be a valid image ID + if err := utils.ValidateID(name); err == nil { + return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) + } + } else { + namespace = nameParts[0] + name = nameParts[1] + } + 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) + } + if !validRepo.MatchString(name) { + return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name) + } + return nil +} + +func validateNoSchema(reposName string) error { + if strings.Contains(reposName, "://") { + // It cannot contain a scheme! + return ErrInvalidRepositoryName + } + return 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) +} + +// NewIndexInfo returns IndexInfo configuration from indexName +func (config *ServiceConfig) NewIndexInfo(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 +} + +// 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 +} + +// splitReposName breaks a reposName into an index name and remote name +func splitReposName(reposName string) (string, string) { + nameParts := strings.SplitN(reposName, "/", 2) + var indexName, remoteName string + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && + !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Index repos (ex: samalba/hipache or ubuntu) + // '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 (config *ServiceConfig) NewRepositoryInfo(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 + } + + repoInfo := &RepositoryInfo{ + RemoteName: remoteName, + } + + var err error + repoInfo.Index, err = config.NewIndexInfo(indexName) + if err != nil { + return nil, err + } + + 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 +} + +// 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 +} + +// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but +// lacks registry configuration. +func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) { + return emptyServiceConfig.NewRepositoryInfo(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 +} diff --git a/docs/endpoint.go b/docs/endpoint.go index 86f53744..95680c5e 100644 --- a/docs/endpoint.go +++ b/docs/endpoint.go @@ -157,52 +157,3 @@ func (e Endpoint) Ping() (RegistryInfo, error) { log.Debugf("RegistryInfo.Standalone: %t", info.Standalone) return info, nil } - -// isSecureIndex returns false if the provided indexName is part of the list of insecure registries -// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. -// -// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered -// insecure. -// -// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name -// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element -// of insecureRegistries. -func (config *ServiceConfig) isSecureIndex(indexName string) bool { - // Check for configured index, first. This is needed in case isSecureIndex - // is called from anything besides NewIndexInfo, in order to honor per-index configurations. - if index, ok := config.IndexConfigs[indexName]; ok { - return index.Secure - } - - host, _, err := net.SplitHostPort(indexName) - if err != nil { - // assume indexName is of the form `host` without the port and go on. - host = indexName - } - - addrs, err := lookupIP(host) - if err != nil { - ip := net.ParseIP(host) - if ip != nil { - addrs = []net.IP{ip} - } - - // if ip == nil, then `host` is neither an IP nor it could be looked up, - // either because the index is unreachable, or because the index is behind an HTTP proxy. - // So, len(addrs) == 0 and we're not aborting. - } - - // Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined. - for _, addr := range addrs { - for _, ipnet := range config.InsecureRegistryCIDRs { - // check if the addr falls in the subnet - if (*net.IPNet)(ipnet).Contains(addr) { - return false - } - } - } - - return true -} diff --git a/docs/registry.go b/docs/registry.go index de724ee2..77a78a82 100644 --- a/docs/registry.go +++ b/docs/registry.go @@ -10,7 +10,6 @@ import ( "net/http" "os" "path" - "regexp" "strings" "time" @@ -19,13 +18,9 @@ import ( ) var ( - ErrAlreadyExists = errors.New("Image already exists") - ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") - ErrDoesNotExist = errors.New("Image does not exist") - errLoginRequired = errors.New("Authentication is required.") - validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`) - validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`) - emptyServiceConfig = NewServiceConfig(nil) + ErrAlreadyExists = errors.New("Image already exists") + ErrDoesNotExist = errors.New("Image does not exist") + errLoginRequired = errors.New("Authentication is required.") ) type TimeoutType uint32 @@ -161,185 +156,6 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secur return res, client, err } -func validateRemoteName(remoteName string) error { - var ( - namespace string - name string - ) - nameParts := strings.SplitN(remoteName, "/", 2) - if len(nameParts) < 2 { - namespace = "library" - name = nameParts[0] - - // the repository name must not be a valid image ID - if err := utils.ValidateID(name); err == nil { - return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) - } - } else { - namespace = nameParts[0] - name = nameParts[1] - } - 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) - } - if !validRepo.MatchString(name) { - return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name) - } - return nil -} - -// 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 { - if strings.Contains(reposName, "://") { - // It cannot contain a scheme! - return ErrInvalidRepositoryName - } - return nil -} - -// splitReposName breaks a reposName into an index name and remote name -func splitReposName(reposName string) (string, string) { - nameParts := strings.SplitN(reposName, "/", 2) - var indexName, remoteName string - if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && - !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { - // This is a Docker Index repos (ex: samalba/hipache or ubuntu) - // '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 - } - - repoInfo := &RepositoryInfo{ - RemoteName: remoteName, - } - - var err error - repoInfo.Index, err = NewIndexInfo(config, indexName) - if err != nil { - return nil, err - } - - 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 -} - func trustedLocation(req *http.Request) bool { var ( trusteds = []string{"docker.com", "docker.io"} diff --git a/docs/registry_test.go b/docs/registry_test.go index 511d7eb1..6bf31505 100644 --- a/docs/registry_test.go +++ b/docs/registry_test.go @@ -561,7 +561,7 @@ func TestParseRepositoryInfo(t *testing.T) { func TestNewIndexInfo(t *testing.T) { testIndexInfo := func(config *ServiceConfig, expectedIndexInfos map[string]*IndexInfo) { for indexName, expectedIndexInfo := range expectedIndexInfos { - index, err := NewIndexInfo(config, indexName) + index, err := config.NewIndexInfo(indexName) if err != nil { t.Fatal(err) } else { diff --git a/docs/service.go b/docs/service.go index 310539c4..c34e3842 100644 --- a/docs/service.go +++ b/docs/service.go @@ -130,7 +130,7 @@ func (s *Service) ResolveRepository(job *engine.Job) engine.Status { reposName = job.Args[0] ) - repoInfo, err := NewRepositoryInfo(s.Config, reposName) + repoInfo, err := s.Config.NewRepositoryInfo(reposName) if err != nil { return job.Error(err) } @@ -168,7 +168,7 @@ func (s *Service) ResolveIndex(job *engine.Job) engine.Status { indexName = job.Args[0] ) - index, err := NewIndexInfo(s.Config, indexName) + index, err := s.Config.NewIndexInfo(indexName) if err != nil { return job.Error(err) }