1/* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15*/ 16 17package docker 18 19import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "net/url" 27 "path" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/containerd/containerd/images" 34 "github.com/containerd/containerd/log" 35 "github.com/containerd/containerd/reference" 36 "github.com/containerd/containerd/remotes" 37 digest "github.com/opencontainers/go-digest" 38 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 39 "github.com/pkg/errors" 40 "github.com/sirupsen/logrus" 41 "golang.org/x/net/context/ctxhttp" 42) 43 44var ( 45 // ErrNoToken is returned if a request is successful but the body does not 46 // contain an authorization token. 47 ErrNoToken = errors.New("authorization server did not include a token in the response") 48 49 // ErrInvalidAuthorization is used when credentials are passed to a server but 50 // those credentials are rejected. 51 ErrInvalidAuthorization = errors.New("authorization failed") 52) 53 54type dockerResolver struct { 55 credentials func(string) (string, string, error) 56 host func(string) (string, error) 57 plainHTTP bool 58 client *http.Client 59 tracker StatusTracker 60} 61 62// ResolverOptions are used to configured a new Docker register resolver 63type ResolverOptions struct { 64 // Credentials provides username and secret given a host. 65 // If username is empty but a secret is given, that secret 66 // is interpretted as a long lived token. 67 Credentials func(string) (string, string, error) 68 69 // Host provides the hostname given a namespace. 70 Host func(string) (string, error) 71 72 // PlainHTTP specifies to use plain http and not https 73 PlainHTTP bool 74 75 // Client is the http client to used when making registry requests 76 Client *http.Client 77 78 // Tracker is used to track uploads to the registry. This is used 79 // since the registry does not have upload tracking and the existing 80 // mechanism for getting blob upload status is expensive. 81 Tracker StatusTracker 82} 83 84// DefaultHost is the default host function. 85func DefaultHost(ns string) (string, error) { 86 if ns == "docker.io" { 87 return "registry-1.docker.io", nil 88 } 89 return ns, nil 90} 91 92// NewResolver returns a new resolver to a Docker registry 93func NewResolver(options ResolverOptions) remotes.Resolver { 94 tracker := options.Tracker 95 if tracker == nil { 96 tracker = NewInMemoryTracker() 97 } 98 host := options.Host 99 if host == nil { 100 host = DefaultHost 101 } 102 return &dockerResolver{ 103 credentials: options.Credentials, 104 host: host, 105 plainHTTP: options.PlainHTTP, 106 client: options.Client, 107 tracker: tracker, 108 } 109} 110 111var _ remotes.Resolver = &dockerResolver{} 112 113func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) { 114 refspec, err := reference.Parse(ref) 115 if err != nil { 116 return "", ocispec.Descriptor{}, err 117 } 118 119 if refspec.Object == "" { 120 return "", ocispec.Descriptor{}, reference.ErrObjectRequired 121 } 122 123 base, err := r.base(refspec) 124 if err != nil { 125 return "", ocispec.Descriptor{}, err 126 } 127 128 fetcher := dockerFetcher{ 129 dockerBase: base, 130 } 131 132 var ( 133 urls []string 134 dgst = refspec.Digest() 135 ) 136 137 if dgst != "" { 138 if err := dgst.Validate(); err != nil { 139 // need to fail here, since we can't actually resolve the invalid 140 // digest. 141 return "", ocispec.Descriptor{}, err 142 } 143 144 // turns out, we have a valid digest, make a url. 145 urls = append(urls, fetcher.url("manifests", dgst.String())) 146 147 // fallback to blobs on not found. 148 urls = append(urls, fetcher.url("blobs", dgst.String())) 149 } else { 150 urls = append(urls, fetcher.url("manifests", refspec.Object)) 151 } 152 153 ctx, err = contextWithRepositoryScope(ctx, refspec, false) 154 if err != nil { 155 return "", ocispec.Descriptor{}, err 156 } 157 for _, u := range urls { 158 req, err := http.NewRequest(http.MethodHead, u, nil) 159 if err != nil { 160 return "", ocispec.Descriptor{}, err 161 } 162 163 // set headers for all the types we support for resolution. 164 req.Header.Set("Accept", strings.Join([]string{ 165 images.MediaTypeDockerSchema2Manifest, 166 images.MediaTypeDockerSchema2ManifestList, 167 ocispec.MediaTypeImageManifest, 168 ocispec.MediaTypeImageIndex, "*"}, ", ")) 169 170 log.G(ctx).Debug("resolving") 171 resp, err := fetcher.doRequestWithRetries(ctx, req, nil) 172 if err != nil { 173 if errors.Cause(err) == ErrInvalidAuthorization { 174 err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization") 175 } 176 return "", ocispec.Descriptor{}, err 177 } 178 resp.Body.Close() // don't care about body contents. 179 180 if resp.StatusCode > 299 { 181 if resp.StatusCode == http.StatusNotFound { 182 continue 183 } 184 return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status) 185 } 186 187 // this is the only point at which we trust the registry. we use the 188 // content headers to assemble a descriptor for the name. when this becomes 189 // more robust, we mostly get this information from a secure trust store. 190 dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest")) 191 192 if dgstHeader != "" { 193 if err := dgstHeader.Validate(); err != nil { 194 return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader) 195 } 196 dgst = dgstHeader 197 } 198 199 if dgst == "" { 200 return "", ocispec.Descriptor{}, errors.Errorf("could not resolve digest for %v", ref) 201 } 202 203 var ( 204 size int64 205 sizeHeader = resp.Header.Get("Content-Length") 206 ) 207 208 size, err = strconv.ParseInt(sizeHeader, 10, 64) 209 if err != nil { 210 211 return "", ocispec.Descriptor{}, errors.Wrapf(err, "invalid size header: %q", sizeHeader) 212 } 213 if size < 0 { 214 return "", ocispec.Descriptor{}, errors.Errorf("%q in header not a valid size", sizeHeader) 215 } 216 217 desc := ocispec.Descriptor{ 218 Digest: dgst, 219 MediaType: resp.Header.Get("Content-Type"), // need to strip disposition? 220 Size: size, 221 } 222 223 log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved") 224 return ref, desc, nil 225 } 226 227 return "", ocispec.Descriptor{}, errors.Errorf("%v not found", ref) 228} 229 230func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { 231 refspec, err := reference.Parse(ref) 232 if err != nil { 233 return nil, err 234 } 235 236 base, err := r.base(refspec) 237 if err != nil { 238 return nil, err 239 } 240 241 return dockerFetcher{ 242 dockerBase: base, 243 }, nil 244} 245 246func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { 247 refspec, err := reference.Parse(ref) 248 if err != nil { 249 return nil, err 250 } 251 252 // Manifests can be pushed by digest like any other object, but the passed in 253 // reference cannot take a digest without the associated content. A tag is allowed 254 // and will be used to tag pushed manifests. 255 if refspec.Object != "" && strings.Contains(refspec.Object, "@") { 256 return nil, errors.New("cannot use digest reference for push locator") 257 } 258 259 base, err := r.base(refspec) 260 if err != nil { 261 return nil, err 262 } 263 264 return dockerPusher{ 265 dockerBase: base, 266 tag: refspec.Object, 267 tracker: r.tracker, 268 }, nil 269} 270 271type dockerBase struct { 272 refspec reference.Spec 273 base url.URL 274 275 client *http.Client 276 useBasic bool 277 username, secret string 278 token string 279 mu sync.Mutex 280} 281 282func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) { 283 var ( 284 err error 285 base url.URL 286 username, secret string 287 ) 288 289 host := refspec.Hostname() 290 base.Host = host 291 if r.host != nil { 292 base.Host, err = r.host(host) 293 if err != nil { 294 return nil, err 295 } 296 } 297 298 base.Scheme = "https" 299 if r.plainHTTP || strings.HasPrefix(base.Host, "localhost:") { 300 base.Scheme = "http" 301 } 302 303 if r.credentials != nil { 304 username, secret, err = r.credentials(base.Host) 305 if err != nil { 306 return nil, err 307 } 308 } 309 310 prefix := strings.TrimPrefix(refspec.Locator, host+"/") 311 base.Path = path.Join("/v2", prefix) 312 313 return &dockerBase{ 314 refspec: refspec, 315 base: base, 316 client: r.client, 317 username: username, 318 secret: secret, 319 }, nil 320} 321 322func (r *dockerBase) getToken() string { 323 r.mu.Lock() 324 defer r.mu.Unlock() 325 326 return r.token 327} 328 329func (r *dockerBase) setToken(token string) bool { 330 r.mu.Lock() 331 defer r.mu.Unlock() 332 333 changed := r.token != token 334 r.token = token 335 336 return changed 337} 338 339func (r *dockerBase) url(ps ...string) string { 340 url := r.base 341 url.Path = path.Join(url.Path, path.Join(ps...)) 342 return url.String() 343} 344 345func (r *dockerBase) authorize(req *http.Request) { 346 token := r.getToken() 347 if r.useBasic { 348 req.SetBasicAuth(r.username, r.secret) 349 } else if token != "" { 350 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 351 } 352} 353 354func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { 355 ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String())) 356 log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request") 357 r.authorize(req) 358 resp, err := ctxhttp.Do(ctx, r.client, req) 359 if err != nil { 360 return nil, errors.Wrap(err, "failed to do request") 361 } 362 log.G(ctx).WithFields(logrus.Fields{ 363 "status": resp.Status, 364 "response.headers": resp.Header, 365 }).Debug("fetch response received") 366 return resp, nil 367} 368 369func (r *dockerBase) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) { 370 resp, err := r.doRequest(ctx, req) 371 if err != nil { 372 return nil, err 373 } 374 375 responses = append(responses, resp) 376 req, err = r.retryRequest(ctx, req, responses) 377 if err != nil { 378 resp.Body.Close() 379 return nil, err 380 } 381 if req != nil { 382 resp.Body.Close() 383 return r.doRequestWithRetries(ctx, req, responses) 384 } 385 return resp, err 386} 387 388func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) { 389 if len(responses) > 5 { 390 return nil, nil 391 } 392 last := responses[len(responses)-1] 393 if last.StatusCode == http.StatusUnauthorized { 394 log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized") 395 for _, c := range parseAuthHeader(last.Header) { 396 if c.scheme == bearerAuth { 397 if err := invalidAuthorization(c, responses); err != nil { 398 r.setToken("") 399 return nil, err 400 } 401 if err := r.setTokenAuth(ctx, c.parameters); err != nil { 402 return nil, err 403 } 404 return copyRequest(req) 405 } else if c.scheme == basicAuth { 406 if r.username != "" && r.secret != "" { 407 r.useBasic = true 408 } 409 return copyRequest(req) 410 } 411 } 412 return nil, nil 413 } else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead { 414 // Support registries which have not properly implemented the HEAD method for 415 // manifests endpoint 416 if strings.Contains(req.URL.Path, "/manifests/") { 417 // TODO: copy request? 418 req.Method = http.MethodGet 419 return copyRequest(req) 420 } 421 } 422 423 // TODO: Handle 50x errors accounting for attempt history 424 return nil, nil 425} 426 427func invalidAuthorization(c challenge, responses []*http.Response) error { 428 errStr := c.parameters["error"] 429 if errStr == "" { 430 return nil 431 } 432 433 n := len(responses) 434 if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) { 435 return nil 436 } 437 438 return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr) 439} 440 441func sameRequest(r1, r2 *http.Request) bool { 442 if r1.Method != r2.Method { 443 return false 444 } 445 if *r1.URL != *r2.URL { 446 return false 447 } 448 return true 449} 450 451func copyRequest(req *http.Request) (*http.Request, error) { 452 ireq := *req 453 if ireq.GetBody != nil { 454 var err error 455 ireq.Body, err = ireq.GetBody() 456 if err != nil { 457 return nil, err 458 } 459 } 460 return &ireq, nil 461} 462 463func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) error { 464 realm, ok := params["realm"] 465 if !ok { 466 return errors.New("no realm specified for token auth challenge") 467 } 468 469 realmURL, err := url.Parse(realm) 470 if err != nil { 471 return fmt.Errorf("invalid token auth challenge realm: %s", err) 472 } 473 474 to := tokenOptions{ 475 realm: realmURL.String(), 476 service: params["service"], 477 } 478 479 to.scopes = getTokenScopes(ctx, params) 480 if len(to.scopes) == 0 { 481 return errors.Errorf("no scope specified for token auth challenge") 482 } 483 484 var token string 485 if r.secret != "" { 486 // Credential information is provided, use oauth POST endpoint 487 token, err = r.fetchTokenWithOAuth(ctx, to) 488 if err != nil { 489 return errors.Wrap(err, "failed to fetch oauth token") 490 } 491 } else { 492 // Do request anonymously 493 token, err = r.fetchToken(ctx, to) 494 if err != nil { 495 return errors.Wrap(err, "failed to fetch anonymous token") 496 } 497 } 498 r.setToken(token) 499 500 return nil 501} 502 503type tokenOptions struct { 504 realm string 505 service string 506 scopes []string 507} 508 509type postTokenResponse struct { 510 AccessToken string `json:"access_token"` 511 RefreshToken string `json:"refresh_token"` 512 ExpiresIn int `json:"expires_in"` 513 IssuedAt time.Time `json:"issued_at"` 514 Scope string `json:"scope"` 515} 516 517func (r *dockerBase) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) { 518 form := url.Values{} 519 form.Set("scope", strings.Join(to.scopes, " ")) 520 form.Set("service", to.service) 521 // TODO: Allow setting client_id 522 form.Set("client_id", "containerd-dist-tool") 523 524 if r.username == "" { 525 form.Set("grant_type", "refresh_token") 526 form.Set("refresh_token", r.secret) 527 } else { 528 form.Set("grant_type", "password") 529 form.Set("username", r.username) 530 form.Set("password", r.secret) 531 } 532 533 resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form) 534 if err != nil { 535 return "", err 536 } 537 defer resp.Body.Close() 538 539 // Registries without support for POST may return 404 for POST /v2/token. 540 // As of September 2017, GCR is known to return 404. 541 // As of February 2018, JFrog Artifactory is known to return 401. 542 if (resp.StatusCode == 405 && r.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 { 543 return r.fetchToken(ctx, to) 544 } else if resp.StatusCode < 200 || resp.StatusCode >= 400 { 545 b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB 546 log.G(ctx).WithFields(logrus.Fields{ 547 "status": resp.Status, 548 "body": string(b), 549 }).Debugf("token request failed") 550 // TODO: handle error body and write debug output 551 return "", errors.Errorf("unexpected status: %s", resp.Status) 552 } 553 554 decoder := json.NewDecoder(resp.Body) 555 556 var tr postTokenResponse 557 if err = decoder.Decode(&tr); err != nil { 558 return "", fmt.Errorf("unable to decode token response: %s", err) 559 } 560 561 return tr.AccessToken, nil 562} 563 564type getTokenResponse struct { 565 Token string `json:"token"` 566 AccessToken string `json:"access_token"` 567 ExpiresIn int `json:"expires_in"` 568 IssuedAt time.Time `json:"issued_at"` 569 RefreshToken string `json:"refresh_token"` 570} 571 572// getToken fetches a token using a GET request 573func (r *dockerBase) fetchToken(ctx context.Context, to tokenOptions) (string, error) { 574 req, err := http.NewRequest("GET", to.realm, nil) 575 if err != nil { 576 return "", err 577 } 578 579 reqParams := req.URL.Query() 580 581 if to.service != "" { 582 reqParams.Add("service", to.service) 583 } 584 585 for _, scope := range to.scopes { 586 reqParams.Add("scope", scope) 587 } 588 589 if r.secret != "" { 590 req.SetBasicAuth(r.username, r.secret) 591 } 592 593 req.URL.RawQuery = reqParams.Encode() 594 595 resp, err := ctxhttp.Do(ctx, r.client, req) 596 if err != nil { 597 return "", err 598 } 599 defer resp.Body.Close() 600 601 if resp.StatusCode < 200 || resp.StatusCode >= 400 { 602 // TODO: handle error body and write debug output 603 return "", errors.Errorf("unexpected status: %s", resp.Status) 604 } 605 606 decoder := json.NewDecoder(resp.Body) 607 608 var tr getTokenResponse 609 if err = decoder.Decode(&tr); err != nil { 610 return "", fmt.Errorf("unable to decode token response: %s", err) 611 } 612 613 // `access_token` is equivalent to `token` and if both are specified 614 // the choice is undefined. Canonicalize `access_token` by sticking 615 // things in `token`. 616 if tr.AccessToken != "" { 617 tr.Token = tr.AccessToken 618 } 619 620 if tr.Token == "" { 621 return "", ErrNoToken 622 } 623 624 return tr.Token, nil 625} 626