github.com/vincentwoo/docker@v0.7.3-0.20160116130405-82401a4b13c0/registry/endpoint.go (about) 1 package registry 2 3 import ( 4 "crypto/tls" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net" 9 "net/http" 10 "net/url" 11 "strings" 12 13 "github.com/Sirupsen/logrus" 14 "github.com/docker/distribution/registry/api/v2" 15 "github.com/docker/distribution/registry/client/transport" 16 registrytypes "github.com/docker/engine-api/types/registry" 17 ) 18 19 // for mocking in unit tests 20 var lookupIP = net.LookupIP 21 22 // scans string for api version in the URL path. returns the trimmed address, if version found, string and API version. 23 func scanForAPIVersion(address string) (string, APIVersion) { 24 var ( 25 chunks []string 26 apiVersionStr string 27 ) 28 29 if strings.HasSuffix(address, "/") { 30 address = address[:len(address)-1] 31 } 32 33 chunks = strings.Split(address, "/") 34 apiVersionStr = chunks[len(chunks)-1] 35 36 for k, v := range apiVersions { 37 if apiVersionStr == v { 38 address = strings.Join(chunks[:len(chunks)-1], "/") 39 return address, k 40 } 41 } 42 43 return address, APIVersionUnknown 44 } 45 46 // NewEndpoint parses the given address to return a registry endpoint. v can be used to 47 // specify a specific endpoint version 48 func NewEndpoint(index *registrytypes.IndexInfo, metaHeaders http.Header, v APIVersion) (*Endpoint, error) { 49 tlsConfig, err := newTLSConfig(index.Name, index.Secure) 50 if err != nil { 51 return nil, err 52 } 53 endpoint, err := newEndpoint(GetAuthConfigKey(index), tlsConfig, metaHeaders) 54 if err != nil { 55 return nil, err 56 } 57 if v != APIVersionUnknown { 58 endpoint.Version = v 59 } 60 if err := validateEndpoint(endpoint); err != nil { 61 return nil, err 62 } 63 64 return endpoint, nil 65 } 66 67 func validateEndpoint(endpoint *Endpoint) error { 68 logrus.Debugf("pinging registry endpoint %s", endpoint) 69 70 // Try HTTPS ping to registry 71 endpoint.URL.Scheme = "https" 72 if _, err := endpoint.Ping(); err != nil { 73 if endpoint.IsSecure { 74 // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` 75 // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP. 76 return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) 77 } 78 79 // If registry is insecure and HTTPS failed, fallback to HTTP. 80 logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err) 81 endpoint.URL.Scheme = "http" 82 83 var err2 error 84 if _, err2 = endpoint.Ping(); err2 == nil { 85 return nil 86 } 87 88 return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) 89 } 90 91 return nil 92 } 93 94 func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) (*Endpoint, error) { 95 var ( 96 endpoint = new(Endpoint) 97 trimmedAddress string 98 err error 99 ) 100 101 if !strings.HasPrefix(address, "http") { 102 address = "https://" + address 103 } 104 105 endpoint.IsSecure = (tlsConfig == nil || !tlsConfig.InsecureSkipVerify) 106 107 trimmedAddress, endpoint.Version = scanForAPIVersion(address) 108 109 if endpoint.URL, err = url.Parse(trimmedAddress); err != nil { 110 return nil, err 111 } 112 113 // TODO(tiborvass): make sure a ConnectTimeout transport is used 114 tr := NewTransport(tlsConfig) 115 endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...)) 116 return endpoint, nil 117 } 118 119 // Endpoint stores basic information about a registry endpoint. 120 type Endpoint struct { 121 client *http.Client 122 URL *url.URL 123 Version APIVersion 124 IsSecure bool 125 AuthChallenges []*AuthorizationChallenge 126 URLBuilder *v2.URLBuilder 127 } 128 129 // Get the formatted URL for the root of this registry Endpoint 130 func (e *Endpoint) String() string { 131 return fmt.Sprintf("%s/v%d/", e.URL, e.Version) 132 } 133 134 // VersionString returns a formatted string of this 135 // endpoint address using the given API Version. 136 func (e *Endpoint) VersionString(version APIVersion) string { 137 return fmt.Sprintf("%s/v%d/", e.URL, version) 138 } 139 140 // Path returns a formatted string for the URL 141 // of this endpoint with the given path appended. 142 func (e *Endpoint) Path(path string) string { 143 return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path) 144 } 145 146 // Ping pings the remote endpoint with v2 and v1 pings to determine the API 147 // version. It returns a PingResult containing the discovered version. The 148 // PingResult also indicates whether the registry is standalone or not. 149 func (e *Endpoint) Ping() (PingResult, error) { 150 // The ping logic to use is determined by the registry endpoint version. 151 switch e.Version { 152 case APIVersion1: 153 return e.pingV1() 154 case APIVersion2: 155 return e.pingV2() 156 } 157 158 // APIVersionUnknown 159 // We should try v2 first... 160 e.Version = APIVersion2 161 regInfo, errV2 := e.pingV2() 162 if errV2 == nil { 163 return regInfo, nil 164 } 165 166 // ... then fallback to v1. 167 e.Version = APIVersion1 168 regInfo, errV1 := e.pingV1() 169 if errV1 == nil { 170 return regInfo, nil 171 } 172 173 e.Version = APIVersionUnknown 174 return PingResult{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1) 175 } 176 177 func (e *Endpoint) pingV1() (PingResult, error) { 178 logrus.Debugf("attempting v1 ping for registry endpoint %s", e) 179 180 if e.String() == IndexServer { 181 // Skip the check, we know this one is valid 182 // (and we never want to fallback to http in case of error) 183 return PingResult{Standalone: false}, nil 184 } 185 186 req, err := http.NewRequest("GET", e.Path("_ping"), nil) 187 if err != nil { 188 return PingResult{Standalone: false}, err 189 } 190 191 resp, err := e.client.Do(req) 192 if err != nil { 193 return PingResult{Standalone: false}, err 194 } 195 196 defer resp.Body.Close() 197 198 jsonString, err := ioutil.ReadAll(resp.Body) 199 if err != nil { 200 return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err) 201 } 202 203 // If the header is absent, we assume true for compatibility with earlier 204 // versions of the registry. default to true 205 info := PingResult{ 206 Standalone: true, 207 } 208 if err := json.Unmarshal(jsonString, &info); err != nil { 209 logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err) 210 // don't stop here. Just assume sane defaults 211 } 212 if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" { 213 logrus.Debugf("Registry version header: '%s'", hdr) 214 info.Version = hdr 215 } 216 logrus.Debugf("PingResult.Version: %q", info.Version) 217 218 standalone := resp.Header.Get("X-Docker-Registry-Standalone") 219 logrus.Debugf("Registry standalone header: '%s'", standalone) 220 // Accepted values are "true" (case-insensitive) and "1". 221 if strings.EqualFold(standalone, "true") || standalone == "1" { 222 info.Standalone = true 223 } else if len(standalone) > 0 { 224 // there is a header set, and it is not "true" or "1", so assume fails 225 info.Standalone = false 226 } 227 logrus.Debugf("PingResult.Standalone: %t", info.Standalone) 228 return info, nil 229 } 230 231 func (e *Endpoint) pingV2() (PingResult, error) { 232 logrus.Debugf("attempting v2 ping for registry endpoint %s", e) 233 234 req, err := http.NewRequest("GET", e.Path(""), nil) 235 if err != nil { 236 return PingResult{}, err 237 } 238 239 resp, err := e.client.Do(req) 240 if err != nil { 241 return PingResult{}, err 242 } 243 defer resp.Body.Close() 244 245 // The endpoint may have multiple supported versions. 246 // Ensure it supports the v2 Registry API. 247 var supportsV2 bool 248 249 HeaderLoop: 250 for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] { 251 for _, versionName := range strings.Fields(supportedVersions) { 252 if versionName == "registry/2.0" { 253 supportsV2 = true 254 break HeaderLoop 255 } 256 } 257 } 258 259 if !supportsV2 { 260 return PingResult{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e) 261 } 262 263 if resp.StatusCode == http.StatusOK { 264 // It would seem that no authentication/authorization is required. 265 // So we don't need to parse/add any authorization schemes. 266 return PingResult{Standalone: true}, nil 267 } 268 269 if resp.StatusCode == http.StatusUnauthorized { 270 // Parse the WWW-Authenticate Header and store the challenges 271 // on this endpoint object. 272 e.AuthChallenges = parseAuthHeader(resp.Header) 273 return PingResult{}, nil 274 } 275 276 return PingResult{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode)) 277 }