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  }