github.com/containerd/Containerd@v1.4.13/remotes/docker/resolver.go (about) 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 17 package docker 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "net/http" 25 "net/url" 26 "path" 27 "strings" 28 29 "github.com/containerd/containerd/errdefs" 30 "github.com/containerd/containerd/images" 31 "github.com/containerd/containerd/log" 32 "github.com/containerd/containerd/reference" 33 "github.com/containerd/containerd/remotes" 34 "github.com/containerd/containerd/remotes/docker/schema1" 35 "github.com/containerd/containerd/version" 36 digest "github.com/opencontainers/go-digest" 37 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 38 "github.com/pkg/errors" 39 "github.com/sirupsen/logrus" 40 "golang.org/x/net/context/ctxhttp" 41 ) 42 43 var ( 44 // ErrNoToken is returned if a request is successful but the body does not 45 // contain an authorization token. 46 ErrNoToken = errors.New("authorization server did not include a token in the response") 47 48 // ErrInvalidAuthorization is used when credentials are passed to a server but 49 // those credentials are rejected. 50 ErrInvalidAuthorization = errors.New("authorization failed") 51 52 // MaxManifestSize represents the largest size accepted from a registry 53 // during resolution. Larger manifests may be accepted using a 54 // resolution method other than the registry. 55 // 56 // NOTE: The max supported layers by some runtimes is 128 and individual 57 // layers will not contribute more than 256 bytes, making a 58 // reasonable limit for a large image manifests of 32K bytes. 59 // 4M bytes represents a much larger upper bound for images which may 60 // contain large annotations or be non-images. A proper manifest 61 // design puts large metadata in subobjects, as is consistent the 62 // intent of the manifest design. 63 MaxManifestSize int64 = 4 * 1048 * 1048 64 ) 65 66 // Authorizer is used to authorize HTTP requests based on 401 HTTP responses. 67 // An Authorizer is responsible for caching tokens or credentials used by 68 // requests. 69 type Authorizer interface { 70 // Authorize sets the appropriate `Authorization` header on the given 71 // request. 72 // 73 // If no authorization is found for the request, the request remains 74 // unmodified. It may also add an `Authorization` header as 75 // "bearer <some bearer token>" 76 // "basic <base64 encoded credentials>" 77 Authorize(context.Context, *http.Request) error 78 79 // AddResponses adds a 401 response for the authorizer to consider when 80 // authorizing requests. The last response should be unauthorized and 81 // the previous requests are used to consider redirects and retries 82 // that may have led to the 401. 83 // 84 // If response is not handled, returns `ErrNotImplemented` 85 AddResponses(context.Context, []*http.Response) error 86 } 87 88 // ResolverOptions are used to configured a new Docker register resolver 89 type ResolverOptions struct { 90 // Hosts returns registry host configurations for a namespace. 91 Hosts RegistryHosts 92 93 // Headers are the HTTP request header fields sent by the resolver 94 Headers http.Header 95 96 // Tracker is used to track uploads to the registry. This is used 97 // since the registry does not have upload tracking and the existing 98 // mechanism for getting blob upload status is expensive. 99 Tracker StatusTracker 100 101 // Authorizer is used to authorize registry requests 102 // Deprecated: use Hosts 103 Authorizer Authorizer 104 105 // Credentials provides username and secret given a host. 106 // If username is empty but a secret is given, that secret 107 // is interpreted as a long lived token. 108 // Deprecated: use Hosts 109 Credentials func(string) (string, string, error) 110 111 // Host provides the hostname given a namespace. 112 // Deprecated: use Hosts 113 Host func(string) (string, error) 114 115 // PlainHTTP specifies to use plain http and not https 116 // Deprecated: use Hosts 117 PlainHTTP bool 118 119 // Client is the http client to used when making registry requests 120 // Deprecated: use Hosts 121 Client *http.Client 122 } 123 124 // DefaultHost is the default host function. 125 func DefaultHost(ns string) (string, error) { 126 if ns == "docker.io" { 127 return "registry-1.docker.io", nil 128 } 129 return ns, nil 130 } 131 132 type dockerResolver struct { 133 hosts RegistryHosts 134 header http.Header 135 resolveHeader http.Header 136 tracker StatusTracker 137 } 138 139 // NewResolver returns a new resolver to a Docker registry 140 func NewResolver(options ResolverOptions) remotes.Resolver { 141 if options.Tracker == nil { 142 options.Tracker = NewInMemoryTracker() 143 } 144 145 if options.Headers == nil { 146 options.Headers = make(http.Header) 147 } 148 if _, ok := options.Headers["User-Agent"]; !ok { 149 options.Headers.Set("User-Agent", "containerd/"+version.Version) 150 } 151 152 resolveHeader := http.Header{} 153 if _, ok := options.Headers["Accept"]; !ok { 154 // set headers for all the types we support for resolution. 155 resolveHeader.Set("Accept", strings.Join([]string{ 156 images.MediaTypeDockerSchema2Manifest, 157 images.MediaTypeDockerSchema2ManifestList, 158 ocispec.MediaTypeImageManifest, 159 ocispec.MediaTypeImageIndex, "*/*"}, ", ")) 160 } else { 161 resolveHeader["Accept"] = options.Headers["Accept"] 162 delete(options.Headers, "Accept") 163 } 164 165 if options.Hosts == nil { 166 opts := []RegistryOpt{} 167 if options.Host != nil { 168 opts = append(opts, WithHostTranslator(options.Host)) 169 } 170 171 if options.Authorizer == nil { 172 options.Authorizer = NewDockerAuthorizer( 173 WithAuthClient(options.Client), 174 WithAuthHeader(options.Headers), 175 WithAuthCreds(options.Credentials)) 176 } 177 opts = append(opts, WithAuthorizer(options.Authorizer)) 178 179 if options.Client != nil { 180 opts = append(opts, WithClient(options.Client)) 181 } 182 if options.PlainHTTP { 183 opts = append(opts, WithPlainHTTP(MatchAllHosts)) 184 } else { 185 opts = append(opts, WithPlainHTTP(MatchLocalhost)) 186 } 187 options.Hosts = ConfigureDefaultRegistries(opts...) 188 } 189 return &dockerResolver{ 190 hosts: options.Hosts, 191 header: options.Headers, 192 resolveHeader: resolveHeader, 193 tracker: options.Tracker, 194 } 195 } 196 197 func getManifestMediaType(resp *http.Response) string { 198 // Strip encoding data (manifests should always be ascii JSON) 199 contentType := resp.Header.Get("Content-Type") 200 if sp := strings.IndexByte(contentType, ';'); sp != -1 { 201 contentType = contentType[0:sp] 202 } 203 204 // As of Apr 30 2019 the registry.access.redhat.com registry does not specify 205 // the content type of any data but uses schema1 manifests. 206 if contentType == "text/plain" { 207 contentType = images.MediaTypeDockerSchema1Manifest 208 } 209 return contentType 210 } 211 212 type countingReader struct { 213 reader io.Reader 214 bytesRead int64 215 } 216 217 func (r *countingReader) Read(p []byte) (int, error) { 218 n, err := r.reader.Read(p) 219 r.bytesRead += int64(n) 220 return n, err 221 } 222 223 var _ remotes.Resolver = &dockerResolver{} 224 225 func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) { 226 refspec, err := reference.Parse(ref) 227 if err != nil { 228 return "", ocispec.Descriptor{}, err 229 } 230 231 if refspec.Object == "" { 232 return "", ocispec.Descriptor{}, reference.ErrObjectRequired 233 } 234 235 base, err := r.base(refspec) 236 if err != nil { 237 return "", ocispec.Descriptor{}, err 238 } 239 240 var ( 241 firstErr error 242 paths [][]string 243 dgst = refspec.Digest() 244 caps = HostCapabilityPull 245 ) 246 247 if dgst != "" { 248 if err := dgst.Validate(); err != nil { 249 // need to fail here, since we can't actually resolve the invalid 250 // digest. 251 return "", ocispec.Descriptor{}, err 252 } 253 254 // turns out, we have a valid digest, make a url. 255 paths = append(paths, []string{"manifests", dgst.String()}) 256 257 // fallback to blobs on not found. 258 paths = append(paths, []string{"blobs", dgst.String()}) 259 } else { 260 // Add 261 paths = append(paths, []string{"manifests", refspec.Object}) 262 caps |= HostCapabilityResolve 263 } 264 265 hosts := base.filterHosts(caps) 266 if len(hosts) == 0 { 267 return "", ocispec.Descriptor{}, errors.Wrap(errdefs.ErrNotFound, "no resolve hosts") 268 } 269 270 ctx, err = contextWithRepositoryScope(ctx, refspec, false) 271 if err != nil { 272 return "", ocispec.Descriptor{}, err 273 } 274 275 for _, u := range paths { 276 for _, host := range hosts { 277 ctx := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host)) 278 279 req := base.request(host, http.MethodHead, u...) 280 if err := req.addNamespace(base.refspec.Hostname()); err != nil { 281 return "", ocispec.Descriptor{}, err 282 } 283 284 for key, value := range r.resolveHeader { 285 req.header[key] = append(req.header[key], value...) 286 } 287 288 log.G(ctx).Debug("resolving") 289 resp, err := req.doWithRetries(ctx, nil) 290 if err != nil { 291 if errors.Is(err, ErrInvalidAuthorization) { 292 err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization") 293 } 294 // Store the error for referencing later 295 if firstErr == nil { 296 firstErr = err 297 } 298 log.G(ctx).WithError(err).Info("trying next host") 299 continue // try another host 300 } 301 resp.Body.Close() // don't care about body contents. 302 303 if resp.StatusCode > 299 { 304 if resp.StatusCode == http.StatusNotFound { 305 log.G(ctx).Info("trying next host - response was http.StatusNotFound") 306 continue 307 } 308 if resp.StatusCode > 399 { 309 // Set firstErr when encountering the first non-404 status code. 310 if firstErr == nil { 311 firstErr = errors.Errorf("pulling from host %s failed with status code %v: %v", host.Host, u, resp.Status) 312 } 313 continue // try another host 314 } 315 return "", ocispec.Descriptor{}, errors.Errorf("pulling from host %s failed with unexpected status code %v: %v", host.Host, u, resp.Status) 316 } 317 size := resp.ContentLength 318 contentType := getManifestMediaType(resp) 319 320 // if no digest was provided, then only a resolve 321 // trusted registry was contacted, in this case use 322 // the digest header (or content from GET) 323 if dgst == "" { 324 // this is the only point at which we trust the registry. we use the 325 // content headers to assemble a descriptor for the name. when this becomes 326 // more robust, we mostly get this information from a secure trust store. 327 dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest")) 328 329 if dgstHeader != "" && size != -1 { 330 if err := dgstHeader.Validate(); err != nil { 331 return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader) 332 } 333 dgst = dgstHeader 334 } 335 } 336 if dgst == "" || size == -1 { 337 log.G(ctx).Debug("no Docker-Content-Digest header, fetching manifest instead") 338 339 req = base.request(host, http.MethodGet, u...) 340 if err := req.addNamespace(base.refspec.Hostname()); err != nil { 341 return "", ocispec.Descriptor{}, err 342 } 343 344 for key, value := range r.resolveHeader { 345 req.header[key] = append(req.header[key], value...) 346 } 347 348 resp, err := req.doWithRetries(ctx, nil) 349 if err != nil { 350 return "", ocispec.Descriptor{}, err 351 } 352 defer resp.Body.Close() 353 354 bodyReader := countingReader{reader: resp.Body} 355 356 contentType = getManifestMediaType(resp) 357 if dgst == "" { 358 if contentType == images.MediaTypeDockerSchema1Manifest { 359 b, err := schema1.ReadStripSignature(&bodyReader) 360 if err != nil { 361 return "", ocispec.Descriptor{}, err 362 } 363 364 dgst = digest.FromBytes(b) 365 } else { 366 dgst, err = digest.FromReader(&bodyReader) 367 if err != nil { 368 return "", ocispec.Descriptor{}, err 369 } 370 } 371 } else if _, err := io.Copy(ioutil.Discard, &bodyReader); err != nil { 372 return "", ocispec.Descriptor{}, err 373 } 374 size = bodyReader.bytesRead 375 } 376 // Prevent resolving to excessively large manifests 377 if size > MaxManifestSize { 378 if firstErr == nil { 379 firstErr = errors.Wrapf(errdefs.ErrNotFound, "rejecting %d byte manifest for %s", size, ref) 380 } 381 continue 382 } 383 384 desc := ocispec.Descriptor{ 385 Digest: dgst, 386 MediaType: contentType, 387 Size: size, 388 } 389 390 log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved") 391 return ref, desc, nil 392 } 393 } 394 395 // If above loop terminates without return, then there was an error. 396 // "firstErr" contains the first non-404 error. That is, "firstErr == nil" 397 // means that either no registries were given or each registry returned 404. 398 399 if firstErr == nil { 400 firstErr = errors.Wrap(errdefs.ErrNotFound, ref) 401 } 402 403 return "", ocispec.Descriptor{}, firstErr 404 } 405 406 func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { 407 refspec, err := reference.Parse(ref) 408 if err != nil { 409 return nil, err 410 } 411 412 base, err := r.base(refspec) 413 if err != nil { 414 return nil, err 415 } 416 417 return dockerFetcher{ 418 dockerBase: base, 419 }, nil 420 } 421 422 func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { 423 refspec, err := reference.Parse(ref) 424 if err != nil { 425 return nil, err 426 } 427 428 base, err := r.base(refspec) 429 if err != nil { 430 return nil, err 431 } 432 433 return dockerPusher{ 434 dockerBase: base, 435 object: refspec.Object, 436 tracker: r.tracker, 437 }, nil 438 } 439 440 type dockerBase struct { 441 refspec reference.Spec 442 repository string 443 hosts []RegistryHost 444 header http.Header 445 } 446 447 func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) { 448 host := refspec.Hostname() 449 hosts, err := r.hosts(host) 450 if err != nil { 451 return nil, err 452 } 453 return &dockerBase{ 454 refspec: refspec, 455 repository: strings.TrimPrefix(refspec.Locator, host+"/"), 456 hosts: hosts, 457 header: r.header, 458 }, nil 459 } 460 461 func (r *dockerBase) filterHosts(caps HostCapabilities) (hosts []RegistryHost) { 462 for _, host := range r.hosts { 463 if host.Capabilities.Has(caps) { 464 hosts = append(hosts, host) 465 } 466 } 467 return 468 } 469 470 func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *request { 471 header := http.Header{} 472 for key, value := range r.header { 473 header[key] = append(header[key], value...) 474 } 475 for key, value := range host.Header { 476 header[key] = append(header[key], value...) 477 } 478 parts := append([]string{"/", host.Path, r.repository}, ps...) 479 p := path.Join(parts...) 480 // Join strips trailing slash, re-add ending "/" if included 481 if len(parts) > 0 && strings.HasSuffix(parts[len(parts)-1], "/") { 482 p = p + "/" 483 } 484 return &request{ 485 method: method, 486 path: p, 487 header: header, 488 host: host, 489 } 490 } 491 492 func (r *request) authorize(ctx context.Context, req *http.Request) error { 493 // Check if has header for host 494 if r.host.Authorizer != nil { 495 if err := r.host.Authorizer.Authorize(ctx, req); err != nil { 496 return err 497 } 498 } 499 500 return nil 501 } 502 503 func (r *request) addNamespace(ns string) (err error) { 504 if !r.host.isProxy(ns) { 505 return nil 506 } 507 var q url.Values 508 // Parse query 509 if i := strings.IndexByte(r.path, '?'); i > 0 { 510 r.path = r.path[:i+1] 511 q, err = url.ParseQuery(r.path[i+1:]) 512 if err != nil { 513 return 514 } 515 } else { 516 r.path = r.path + "?" 517 q = url.Values{} 518 } 519 q.Add("ns", ns) 520 521 r.path = r.path + q.Encode() 522 523 return 524 } 525 526 type request struct { 527 method string 528 path string 529 header http.Header 530 host RegistryHost 531 body func() (io.ReadCloser, error) 532 size int64 533 } 534 535 func (r *request) do(ctx context.Context) (*http.Response, error) { 536 u := r.host.Scheme + "://" + r.host.Host + r.path 537 req, err := http.NewRequest(r.method, u, nil) 538 if err != nil { 539 return nil, err 540 } 541 req.Header = http.Header{} // headers need to be copied to avoid concurrent map access 542 for k, v := range r.header { 543 req.Header[k] = v 544 } 545 if r.body != nil { 546 body, err := r.body() 547 if err != nil { 548 return nil, err 549 } 550 req.Body = body 551 req.GetBody = r.body 552 if r.size > 0 { 553 req.ContentLength = r.size 554 } 555 } 556 557 ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", u)) 558 log.G(ctx).WithFields(requestFields(req)).Debug("do request") 559 if err := r.authorize(ctx, req); err != nil { 560 return nil, errors.Wrap(err, "failed to authorize") 561 } 562 563 var client = &http.Client{} 564 if r.host.Client != nil { 565 *client = *r.host.Client 566 } 567 if client.CheckRedirect == nil { 568 client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 569 if len(via) >= 10 { 570 return errors.New("stopped after 10 redirects") 571 } 572 return errors.Wrap(r.authorize(ctx, req), "failed to authorize redirect") 573 } 574 } 575 576 resp, err := ctxhttp.Do(ctx, client, req) 577 if err != nil { 578 return nil, errors.Wrap(err, "failed to do request") 579 } 580 log.G(ctx).WithFields(responseFields(resp)).Debug("fetch response received") 581 return resp, nil 582 } 583 584 func (r *request) doWithRetries(ctx context.Context, responses []*http.Response) (*http.Response, error) { 585 resp, err := r.do(ctx) 586 if err != nil { 587 return nil, err 588 } 589 590 responses = append(responses, resp) 591 retry, err := r.retryRequest(ctx, responses) 592 if err != nil { 593 resp.Body.Close() 594 return nil, err 595 } 596 if retry { 597 resp.Body.Close() 598 return r.doWithRetries(ctx, responses) 599 } 600 return resp, err 601 } 602 603 func (r *request) retryRequest(ctx context.Context, responses []*http.Response) (bool, error) { 604 if len(responses) > 5 { 605 return false, nil 606 } 607 last := responses[len(responses)-1] 608 switch last.StatusCode { 609 case http.StatusUnauthorized: 610 log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized") 611 if r.host.Authorizer != nil { 612 if err := r.host.Authorizer.AddResponses(ctx, responses); err == nil { 613 return true, nil 614 } else if !errdefs.IsNotImplemented(err) { 615 return false, err 616 } 617 } 618 619 return false, nil 620 case http.StatusMethodNotAllowed: 621 // Support registries which have not properly implemented the HEAD method for 622 // manifests endpoint 623 if r.method == http.MethodHead && strings.Contains(r.path, "/manifests/") { 624 r.method = http.MethodGet 625 return true, nil 626 } 627 case http.StatusRequestTimeout, http.StatusTooManyRequests: 628 return true, nil 629 } 630 631 // TODO: Handle 50x errors accounting for attempt history 632 return false, nil 633 } 634 635 func (r *request) String() string { 636 return r.host.Scheme + "://" + r.host.Host + r.path 637 } 638 639 func requestFields(req *http.Request) logrus.Fields { 640 fields := map[string]interface{}{ 641 "request.method": req.Method, 642 } 643 for k, vals := range req.Header { 644 k = strings.ToLower(k) 645 if k == "authorization" { 646 continue 647 } 648 for i, v := range vals { 649 field := "request.header." + k 650 if i > 0 { 651 field = fmt.Sprintf("%s.%d", field, i) 652 } 653 fields[field] = v 654 } 655 } 656 657 return logrus.Fields(fields) 658 } 659 660 func responseFields(resp *http.Response) logrus.Fields { 661 fields := map[string]interface{}{ 662 "response.status": resp.Status, 663 } 664 for k, vals := range resp.Header { 665 k = strings.ToLower(k) 666 for i, v := range vals { 667 field := "response.header." + k 668 if i > 0 { 669 field = fmt.Sprintf("%s.%d", field, i) 670 } 671 fields[field] = v 672 } 673 } 674 675 return logrus.Fields(fields) 676 }