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
17package docker
18
19import (
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
43var (
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.
69type 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
89type 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.
125func DefaultHost(ns string) (string, error) {
126	if ns == "docker.io" {
127		return "registry-1.docker.io", nil
128	}
129	return ns, nil
130}
131
132type 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
140func 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
197func 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
212type countingReader struct {
213	reader    io.Reader
214	bytesRead int64
215}
216
217func (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
223var _ remotes.Resolver = &dockerResolver{}
224
225func (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
393func (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
409func (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
427type dockerBase struct {
428	refspec    reference.Spec
429	repository string
430	hosts      []RegistryHost
431	header     http.Header
432}
433
434func (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
448func (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
457func (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
479func (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
490func (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
513type 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
522func (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
554func (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
573func (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
605func (r *request) String() string {
606	return r.host.Scheme + "://" + r.host.Host + r.path
607}
608
609func 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
630func 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}
647