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  }