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 }