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