2013-05-15 01:41:39 +00:00
|
|
|
package registry
|
|
|
|
|
|
|
|
import (
|
2013-12-04 14:03:51 +00:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2013-05-15 20:22:57 +00:00
|
|
|
"errors"
|
2013-05-15 01:41:39 +00:00
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2013-08-05 00:42:24 +00:00
|
|
|
"net"
|
2013-05-15 01:41:39 +00:00
|
|
|
"net/http"
|
2013-12-04 14:03:51 +00:00
|
|
|
"os"
|
|
|
|
"path"
|
2013-07-05 19:20:58 +00:00
|
|
|
"regexp"
|
2013-05-15 01:41:39 +00:00
|
|
|
"strings"
|
2013-08-05 00:42:24 +00:00
|
|
|
"time"
|
2014-04-29 09:01:07 +00:00
|
|
|
|
2014-07-24 22:19:50 +00:00
|
|
|
"github.com/docker/docker/utils"
|
2013-05-15 01:41:39 +00:00
|
|
|
)
|
|
|
|
|
2013-07-22 21:50:32 +00:00
|
|
|
var (
|
|
|
|
ErrAlreadyExists = errors.New("Image already exists")
|
|
|
|
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
|
2014-10-02 01:26:06 +00:00
|
|
|
ErrDoesNotExist = errors.New("Image does not exist")
|
2014-02-03 19:38:34 +00:00
|
|
|
errLoginRequired = errors.New("Authentication is required.")
|
2014-08-18 00:50:15 +00:00
|
|
|
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
2014-09-16 03:30:10 +00:00
|
|
|
validNamespace = regexp.MustCompile(`^([a-z0-9_]{4,30})$`)
|
|
|
|
validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
|
2013-07-22 21:50:32 +00:00
|
|
|
)
|
2013-05-15 20:22:57 +00:00
|
|
|
|
2013-12-04 14:03:51 +00:00
|
|
|
type TimeoutType uint32
|
|
|
|
|
|
|
|
const (
|
|
|
|
NoTimeout TimeoutType = iota
|
|
|
|
ReceiveTimeout
|
|
|
|
ConnectTimeout
|
|
|
|
)
|
|
|
|
|
|
|
|
func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType) *http.Client {
|
2014-10-16 02:39:51 +00:00
|
|
|
tlsConfig := tls.Config{
|
|
|
|
RootCAs: roots,
|
|
|
|
// Avoid fallback to SSL protocols < TLS1.0
|
|
|
|
MinVersion: tls.VersionTLS10,
|
|
|
|
}
|
2013-12-04 14:03:51 +00:00
|
|
|
|
|
|
|
if cert != nil {
|
|
|
|
tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
|
|
|
|
}
|
|
|
|
|
|
|
|
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-20 23:45:45 +00:00
|
|
|
d := net.Dialer{Timeout: 5 * time.Second, DualStack: true}
|
|
|
|
|
|
|
|
conn, err := d.Dial(proto, addr)
|
2013-12-04 14:03:51 +00: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-20 23:45:45 +00:00
|
|
|
d := net.Dialer{DualStack: true}
|
|
|
|
|
|
|
|
conn, err := d.Dial(proto, addr)
|
2013-12-04 14:03:51 +00: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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*http.Response, *http.Client, error) {
|
|
|
|
hasFile := func(files []os.FileInfo, name string) bool {
|
|
|
|
for _, f := range files {
|
|
|
|
if f.Name() == name {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
|
|
|
|
fs, err := ioutil.ReadDir(hostDir)
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
pool *x509.CertPool
|
|
|
|
certs []*tls.Certificate
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, f := range fs {
|
|
|
|
if strings.HasSuffix(f.Name(), ".crt") {
|
|
|
|
if pool == nil {
|
|
|
|
pool = x509.NewCertPool()
|
|
|
|
}
|
|
|
|
data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2014-08-25 16:50:18 +00:00
|
|
|
pool.AppendCertsFromPEM(data)
|
2013-12-04 14:03:51 +00:00
|
|
|
}
|
|
|
|
if strings.HasSuffix(f.Name(), ".cert") {
|
|
|
|
certName := f.Name()
|
|
|
|
keyName := certName[:len(certName)-5] + ".key"
|
|
|
|
if !hasFile(fs, keyName) {
|
|
|
|
return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
|
|
|
}
|
2014-08-25 16:50:18 +00:00
|
|
|
cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName))
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
certs = append(certs, &cert)
|
2013-12-04 14:03:51 +00:00
|
|
|
}
|
|
|
|
if strings.HasSuffix(f.Name(), ".key") {
|
|
|
|
keyName := f.Name()
|
|
|
|
certName := keyName[:len(keyName)-4] + ".cert"
|
|
|
|
if !hasFile(fs, certName) {
|
|
|
|
return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(certs) == 0 {
|
|
|
|
client := newClient(jar, pool, nil, timeout)
|
|
|
|
res, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return res, client, nil
|
2014-08-25 16:50:18 +00:00
|
|
|
}
|
|
|
|
for i, cert := range certs {
|
|
|
|
client := newClient(jar, pool, cert, timeout)
|
|
|
|
res, err := client.Do(req)
|
|
|
|
// If this is the last cert, otherwise, continue to next cert if 403 or 5xx
|
On Red Hat Registry Servers we return 404 on certification errors.
We do this to prevent leakage of information, we don't want people
to be able to probe for existing content.
According to RFC 2616, "This status code (404) is commonly used when the server does not
wish to reveal exactly why the request has been refused, or when no other response i
is applicable."
https://www.ietf.org/rfc/rfc2616.txt
10.4.4 403 Forbidden
The server understood the request, but is refusing to fulfill it.
Authorization will not help and the request SHOULD NOT be repeated.
If the request method was not HEAD and the server wishes to make
public why the request has not been fulfilled, it SHOULD describe the
reason for the refusal in the entity. If the server does not wish to
make this information available to the client, the status code 404
(Not Found) can be used instead.
10.4.5 404 Not Found
The server has not found anything matching the Request-URI. No
indication is given of whether the condition is temporary or
permanent. The 410 (Gone) status code SHOULD be used if the server
knows, through some internally configurable mechanism, that an old
resource is permanently unavailable and has no forwarding address.
This status code is commonly used when the server does not wish to
reveal exactly why the request has been refused, or when no other
response is applicable.
When docker is running through its certificates, it should continue
trying with a new certificate even if it gets back a 404 error code.
Docker-DCO-1.1-Signed-off-by: Dan Walsh <dwalsh@redhat.com> (github: rhatdan)
2014-10-14 13:19:45 +00:00
|
|
|
if i == len(certs)-1 || err == nil &&
|
|
|
|
res.StatusCode != 403 &&
|
|
|
|
res.StatusCode != 404 &&
|
|
|
|
res.StatusCode < 500 {
|
2014-08-25 16:50:18 +00:00
|
|
|
return res, client, err
|
2013-12-04 14:03:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil, nil
|
|
|
|
}
|
|
|
|
|
2013-07-05 21:30:43 +00:00
|
|
|
func validateRepositoryName(repositoryName string) error {
|
|
|
|
var (
|
|
|
|
namespace string
|
|
|
|
name string
|
|
|
|
)
|
|
|
|
nameParts := strings.SplitN(repositoryName, "/", 2)
|
|
|
|
if len(nameParts) < 2 {
|
|
|
|
namespace = "library"
|
|
|
|
name = nameParts[0]
|
2014-08-18 00:50:15 +00:00
|
|
|
|
|
|
|
if validHex.MatchString(name) {
|
|
|
|
return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
|
|
|
|
}
|
2013-07-05 21:30:43 +00:00
|
|
|
} else {
|
|
|
|
namespace = nameParts[0]
|
|
|
|
name = nameParts[1]
|
|
|
|
}
|
2013-07-05 19:20:58 +00:00
|
|
|
if !validNamespace.MatchString(namespace) {
|
|
|
|
return fmt.Errorf("Invalid namespace name (%s), only [a-z0-9_] are allowed, size between 4 and 30", namespace)
|
|
|
|
}
|
|
|
|
if !validRepo.MatchString(name) {
|
2013-09-25 15:33:09 +00:00
|
|
|
return fmt.Errorf("Invalid repository name (%s), only [a-z0-9-_.] are allowed", name)
|
2013-07-05 19:20:58 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2014-02-20 22:57:58 +00:00
|
|
|
// Resolves a repository name to a hostname + name
|
2013-07-05 19:20:58 +00:00
|
|
|
func ResolveRepositoryName(reposName string) (string, string, error) {
|
2013-07-09 18:30:12 +00:00
|
|
|
if strings.Contains(reposName, "://") {
|
|
|
|
// It cannot contain a scheme!
|
|
|
|
return "", "", ErrInvalidRepositoryName
|
|
|
|
}
|
2013-07-05 19:20:58 +00:00
|
|
|
nameParts := strings.SplitN(reposName, "/", 2)
|
2014-04-14 23:15:38 +00:00
|
|
|
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") &&
|
|
|
|
nameParts[0] != "localhost") {
|
2013-07-05 19:20:58 +00:00
|
|
|
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
2013-07-05 21:30:43 +00:00
|
|
|
err := validateRepositoryName(reposName)
|
2014-03-11 00:16:58 +00:00
|
|
|
return IndexServerAddress(), reposName, err
|
2013-07-05 19:20:58 +00:00
|
|
|
}
|
|
|
|
hostname := nameParts[0]
|
2013-07-05 21:30:43 +00:00
|
|
|
reposName = nameParts[1]
|
2013-07-09 23:46:55 +00:00
|
|
|
if strings.Contains(hostname, "index.docker.io") {
|
|
|
|
return "", "", fmt.Errorf("Invalid repository name, try \"%s\" instead", reposName)
|
|
|
|
}
|
|
|
|
if err := validateRepositoryName(reposName); err != nil {
|
|
|
|
return "", "", err
|
|
|
|
}
|
2014-02-20 22:57:58 +00:00
|
|
|
|
|
|
|
return hostname, reposName, nil
|
2013-09-03 18:45:49 +00:00
|
|
|
}
|
|
|
|
|
2014-08-16 10:27:04 +00:00
|
|
|
// this method expands the registry name as used in the prefix of a repo
|
|
|
|
// to a full url. if it already is a url, there will be no change.
|
|
|
|
func ExpandAndVerifyRegistryUrl(hostname string, secure bool) (endpoint string, err error) {
|
|
|
|
if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") {
|
|
|
|
// if there is no slash after https:// (8 characters) then we have no path in the url
|
|
|
|
if strings.LastIndex(hostname, "/") < 9 {
|
|
|
|
// there is no path given. Expand with default path
|
|
|
|
hostname = hostname + "/v1/"
|
|
|
|
}
|
|
|
|
if _, err := pingRegistryEndpoint(hostname); err != nil {
|
|
|
|
return "", errors.New("Invalid Registry endpoint: " + err.Error())
|
|
|
|
}
|
|
|
|
return hostname, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// use HTTPS if secure, otherwise use HTTP
|
|
|
|
if secure {
|
|
|
|
endpoint = fmt.Sprintf("https://%s/v1/", hostname)
|
|
|
|
} else {
|
|
|
|
endpoint = fmt.Sprintf("http://%s/v1/", hostname)
|
|
|
|
}
|
|
|
|
_, err = pingRegistryEndpoint(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
//TODO: triggering highland build can be done there without "failing"
|
|
|
|
err = fmt.Errorf("Invalid registry endpoint '%s': %s ", endpoint, err)
|
|
|
|
if secure {
|
|
|
|
err = fmt.Errorf("%s. If this private registry supports only HTTP, please add `--insecure-registry %s` to the daemon's arguments.", err, hostname)
|
|
|
|
}
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return endpoint, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// this method verifies if the provided hostname is part of the list of
|
|
|
|
// insecure registries and returns false if HTTP should be used
|
|
|
|
func IsSecure(hostname string, insecureRegistries []string) (secure bool) {
|
|
|
|
secure = true
|
|
|
|
for _, h := range insecureRegistries {
|
|
|
|
if hostname == h {
|
|
|
|
secure = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if hostname == IndexServerAddress() {
|
|
|
|
secure = true
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-06-05 18:37:37 +00: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 21:17:56 +00:00
|
|
|
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
2014-06-05 18:37:37 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2014-03-26 00:33:17 +00:00
|
|
|
func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
|
|
|
if via != nil && via[0] != nil {
|
2014-06-05 18:37:37 +00:00
|
|
|
if trustedLocation(req) && trustedLocation(via[0]) {
|
|
|
|
req.Header = via[0].Header
|
2014-08-25 16:50:18 +00: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 18:37:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-03-26 00:33:17 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|