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