github.com/lalkh/containerd@v1.4.3/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 lastErr 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 lastErr == nil { 296 lastErr = err 297 } 298 continue // try another host 299 } 300 resp.Body.Close() // don't care about body contents. 301 302 if resp.StatusCode > 299 { 303 if resp.StatusCode == http.StatusNotFound { 304 continue 305 } 306 return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status) 307 } 308 size := resp.ContentLength 309 contentType := getManifestMediaType(resp) 310 311 // if no digest was provided, then only a resolve 312 // trusted registry was contacted, in this case use 313 // the digest header (or content from GET) 314 if dgst == "" { 315 // this is the only point at which we trust the registry. we use the 316 // content headers to assemble a descriptor for the name. when this becomes 317 // more robust, we mostly get this information from a secure trust store. 318 dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest")) 319 320 if dgstHeader != "" && size != -1 { 321 if err := dgstHeader.Validate(); err != nil { 322 return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader) 323 } 324 dgst = dgstHeader 325 } 326 } 327 if dgst == "" || size == -1 { 328 log.G(ctx).Debug("no Docker-Content-Digest header, fetching manifest instead") 329 330 req = base.request(host, http.MethodGet, u...) 331 if err := req.addNamespace(base.refspec.Hostname()); err != nil { 332 return "", ocispec.Descriptor{}, err 333 } 334 335 for key, value := range r.resolveHeader { 336 req.header[key] = append(req.header[key], value...) 337 } 338 339 resp, err := req.doWithRetries(ctx, nil) 340 if err != nil { 341 return "", ocispec.Descriptor{}, err 342 } 343 defer resp.Body.Close() 344 345 bodyReader := countingReader{reader: resp.Body} 346 347 contentType = getManifestMediaType(resp) 348 if dgst == "" { 349 if contentType == images.MediaTypeDockerSchema1Manifest { 350 b, err := schema1.ReadStripSignature(&bodyReader) 351 if err != nil { 352 return "", ocispec.Descriptor{}, err 353 } 354 355 dgst = digest.FromBytes(b) 356 } else { 357 dgst, err = digest.FromReader(&bodyReader) 358 if err != nil { 359 return "", ocispec.Descriptor{}, err 360 } 361 } 362 } else if _, err := io.Copy(ioutil.Discard, &bodyReader); err != nil { 363 return "", ocispec.Descriptor{}, err 364 } 365 size = bodyReader.bytesRead 366 } 367 // Prevent resolving to excessively large manifests 368 if size > MaxManifestSize { 369 if lastErr == nil { 370 lastErr = errors.Wrapf(errdefs.ErrNotFound, "rejecting %d byte manifest for %s", size, ref) 371 } 372 continue 373 } 374 375 desc := ocispec.Descriptor{ 376 Digest: dgst, 377 MediaType: contentType, 378 Size: size, 379 } 380 381 log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved") 382 return ref, desc, nil 383 } 384 } 385 386 if lastErr == nil { 387 lastErr = errors.Wrap(errdefs.ErrNotFound, ref) 388 } 389 390 return "", ocispec.Descriptor{}, lastErr 391 } 392 393 func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { 394 refspec, err := reference.Parse(ref) 395 if err != nil { 396 return nil, err 397 } 398 399 base, err := r.base(refspec) 400 if err != nil { 401 return nil, err 402 } 403 404 return dockerFetcher{ 405 dockerBase: base, 406 }, nil 407 } 408 409 func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { 410 refspec, err := reference.Parse(ref) 411 if err != nil { 412 return nil, err 413 } 414 415 base, err := r.base(refspec) 416 if err != nil { 417 return nil, err 418 } 419 420 return dockerPusher{ 421 dockerBase: base, 422 object: refspec.Object, 423 tracker: r.tracker, 424 }, nil 425 } 426 427 type dockerBase struct { 428 refspec reference.Spec 429 repository string 430 hosts []RegistryHost 431 header http.Header 432 } 433 434 func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) { 435 host := refspec.Hostname() 436 hosts, err := r.hosts(host) 437 if err != nil { 438 return nil, err 439 } 440 return &dockerBase{ 441 refspec: refspec, 442 repository: strings.TrimPrefix(refspec.Locator, host+"/"), 443 hosts: hosts, 444 header: r.header, 445 }, nil 446 } 447 448 func (r *dockerBase) filterHosts(caps HostCapabilities) (hosts []RegistryHost) { 449 for _, host := range r.hosts { 450 if host.Capabilities.Has(caps) { 451 hosts = append(hosts, host) 452 } 453 } 454 return 455 } 456 457 func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *request { 458 header := http.Header{} 459 for key, value := range r.header { 460 header[key] = append(header[key], value...) 461 } 462 for key, value := range host.Header { 463 header[key] = append(header[key], value...) 464 } 465 parts := append([]string{"/", host.Path, r.repository}, ps...) 466 p := path.Join(parts...) 467 // Join strips trailing slash, re-add ending "/" if included 468 if len(parts) > 0 && strings.HasSuffix(parts[len(parts)-1], "/") { 469 p = p + "/" 470 } 471 return &request{ 472 method: method, 473 path: p, 474 header: header, 475 host: host, 476 } 477 } 478 479 func (r *request) authorize(ctx context.Context, req *http.Request) error { 480 // Check if has header for host 481 if r.host.Authorizer != nil { 482 if err := r.host.Authorizer.Authorize(ctx, req); err != nil { 483 return err 484 } 485 } 486 487 return nil 488 } 489 490 func (r *request) addNamespace(ns string) (err error) { 491 if !r.host.isProxy(ns) { 492 return nil 493 } 494 var q url.Values 495 // Parse query 496 if i := strings.IndexByte(r.path, '?'); i > 0 { 497 r.path = r.path[:i+1] 498 q, err = url.ParseQuery(r.path[i+1:]) 499 if err != nil { 500 return 501 } 502 } else { 503 r.path = r.path + "?" 504 q = url.Values{} 505 } 506 q.Add("ns", ns) 507 508 r.path = r.path + q.Encode() 509 510 return 511 } 512 513 type request struct { 514 method string 515 path string 516 header http.Header 517 host RegistryHost 518 body func() (io.ReadCloser, error) 519 size int64 520 } 521 522 func (r *request) do(ctx context.Context) (*http.Response, error) { 523 u := r.host.Scheme + "://" + r.host.Host + r.path 524 req, err := http.NewRequest(r.method, u, nil) 525 if err != nil { 526 return nil, err 527 } 528 req.Header = r.header 529 if r.body != nil { 530 body, err := r.body() 531 if err != nil { 532 return nil, err 533 } 534 req.Body = body 535 req.GetBody = r.body 536 if r.size > 0 { 537 req.ContentLength = r.size 538 } 539 } 540 541 ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", u)) 542 log.G(ctx).WithFields(requestFields(req)).Debug("do request") 543 if err := r.authorize(ctx, req); err != nil { 544 return nil, errors.Wrap(err, "failed to authorize") 545 } 546 resp, err := ctxhttp.Do(ctx, r.host.Client, req) 547 if err != nil { 548 return nil, errors.Wrap(err, "failed to do request") 549 } 550 log.G(ctx).WithFields(responseFields(resp)).Debug("fetch response received") 551 return resp, nil 552 } 553 554 func (r *request) doWithRetries(ctx context.Context, responses []*http.Response) (*http.Response, error) { 555 resp, err := r.do(ctx) 556 if err != nil { 557 return nil, err 558 } 559 560 responses = append(responses, resp) 561 retry, err := r.retryRequest(ctx, responses) 562 if err != nil { 563 resp.Body.Close() 564 return nil, err 565 } 566 if retry { 567 resp.Body.Close() 568 return r.doWithRetries(ctx, responses) 569 } 570 return resp, err 571 } 572 573 func (r *request) retryRequest(ctx context.Context, responses []*http.Response) (bool, error) { 574 if len(responses) > 5 { 575 return false, nil 576 } 577 last := responses[len(responses)-1] 578 switch last.StatusCode { 579 case http.StatusUnauthorized: 580 log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized") 581 if r.host.Authorizer != nil { 582 if err := r.host.Authorizer.AddResponses(ctx, responses); err == nil { 583 return true, nil 584 } else if !errdefs.IsNotImplemented(err) { 585 return false, err 586 } 587 } 588 589 return false, nil 590 case http.StatusMethodNotAllowed: 591 // Support registries which have not properly implemented the HEAD method for 592 // manifests endpoint 593 if r.method == http.MethodHead && strings.Contains(r.path, "/manifests/") { 594 r.method = http.MethodGet 595 return true, nil 596 } 597 case http.StatusRequestTimeout, http.StatusTooManyRequests: 598 return true, nil 599 } 600 601 // TODO: Handle 50x errors accounting for attempt history 602 return false, nil 603 } 604 605 func (r *request) String() string { 606 return r.host.Scheme + "://" + r.host.Host + r.path 607 } 608 609 func requestFields(req *http.Request) logrus.Fields { 610 fields := map[string]interface{}{ 611 "request.method": req.Method, 612 } 613 for k, vals := range req.Header { 614 k = strings.ToLower(k) 615 if k == "authorization" { 616 continue 617 } 618 for i, v := range vals { 619 field := "request.header." + k 620 if i > 0 { 621 field = fmt.Sprintf("%s.%d", field, i) 622 } 623 fields[field] = v 624 } 625 } 626 627 return logrus.Fields(fields) 628 } 629 630 func responseFields(resp *http.Response) logrus.Fields { 631 fields := map[string]interface{}{ 632 "response.status": resp.Status, 633 } 634 for k, vals := range resp.Header { 635 k = strings.ToLower(k) 636 for i, v := range vals { 637 field := "response.header." + k 638 if i > 0 { 639 field = fmt.Sprintf("%s.%d", field, i) 640 } 641 fields[field] = v 642 } 643 } 644 645 return logrus.Fields(fields) 646 }