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  }