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	"encoding/json"
22	"fmt"
23	"io"
24	"io/ioutil"
25	"net/http"
26	"net/url"
27	"path"
28	"strconv"
29	"strings"
30	"sync"
31	"time"
32
33	"github.com/containerd/containerd/images"
34	"github.com/containerd/containerd/log"
35	"github.com/containerd/containerd/reference"
36	"github.com/containerd/containerd/remotes"
37	digest "github.com/opencontainers/go-digest"
38	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
39	"github.com/pkg/errors"
40	"github.com/sirupsen/logrus"
41	"golang.org/x/net/context/ctxhttp"
42)
43
44var (
45	// ErrNoToken is returned if a request is successful but the body does not
46	// contain an authorization token.
47	ErrNoToken = errors.New("authorization server did not include a token in the response")
48
49	// ErrInvalidAuthorization is used when credentials are passed to a server but
50	// those credentials are rejected.
51	ErrInvalidAuthorization = errors.New("authorization failed")
52)
53
54type dockerResolver struct {
55	credentials func(string) (string, string, error)
56	host        func(string) (string, error)
57	plainHTTP   bool
58	client      *http.Client
59	tracker     StatusTracker
60}
61
62// ResolverOptions are used to configured a new Docker register resolver
63type ResolverOptions struct {
64	// Credentials provides username and secret given a host.
65	// If username is empty but a secret is given, that secret
66	// is interpretted as a long lived token.
67	Credentials func(string) (string, string, error)
68
69	// Host provides the hostname given a namespace.
70	Host func(string) (string, error)
71
72	// PlainHTTP specifies to use plain http and not https
73	PlainHTTP bool
74
75	// Client is the http client to used when making registry requests
76	Client *http.Client
77
78	// Tracker is used to track uploads to the registry. This is used
79	// since the registry does not have upload tracking and the existing
80	// mechanism for getting blob upload status is expensive.
81	Tracker StatusTracker
82}
83
84// DefaultHost is the default host function.
85func DefaultHost(ns string) (string, error) {
86	if ns == "docker.io" {
87		return "registry-1.docker.io", nil
88	}
89	return ns, nil
90}
91
92// NewResolver returns a new resolver to a Docker registry
93func NewResolver(options ResolverOptions) remotes.Resolver {
94	tracker := options.Tracker
95	if tracker == nil {
96		tracker = NewInMemoryTracker()
97	}
98	host := options.Host
99	if host == nil {
100		host = DefaultHost
101	}
102	return &dockerResolver{
103		credentials: options.Credentials,
104		host:        host,
105		plainHTTP:   options.PlainHTTP,
106		client:      options.Client,
107		tracker:     tracker,
108	}
109}
110
111var _ remotes.Resolver = &dockerResolver{}
112
113func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) {
114	refspec, err := reference.Parse(ref)
115	if err != nil {
116		return "", ocispec.Descriptor{}, err
117	}
118
119	if refspec.Object == "" {
120		return "", ocispec.Descriptor{}, reference.ErrObjectRequired
121	}
122
123	base, err := r.base(refspec)
124	if err != nil {
125		return "", ocispec.Descriptor{}, err
126	}
127
128	fetcher := dockerFetcher{
129		dockerBase: base,
130	}
131
132	var (
133		urls []string
134		dgst = refspec.Digest()
135	)
136
137	if dgst != "" {
138		if err := dgst.Validate(); err != nil {
139			// need to fail here, since we can't actually resolve the invalid
140			// digest.
141			return "", ocispec.Descriptor{}, err
142		}
143
144		// turns out, we have a valid digest, make a url.
145		urls = append(urls, fetcher.url("manifests", dgst.String()))
146
147		// fallback to blobs on not found.
148		urls = append(urls, fetcher.url("blobs", dgst.String()))
149	} else {
150		urls = append(urls, fetcher.url("manifests", refspec.Object))
151	}
152
153	ctx, err = contextWithRepositoryScope(ctx, refspec, false)
154	if err != nil {
155		return "", ocispec.Descriptor{}, err
156	}
157	for _, u := range urls {
158		req, err := http.NewRequest(http.MethodHead, u, nil)
159		if err != nil {
160			return "", ocispec.Descriptor{}, err
161		}
162
163		// set headers for all the types we support for resolution.
164		req.Header.Set("Accept", strings.Join([]string{
165			images.MediaTypeDockerSchema2Manifest,
166			images.MediaTypeDockerSchema2ManifestList,
167			ocispec.MediaTypeImageManifest,
168			ocispec.MediaTypeImageIndex, "*"}, ", "))
169
170		log.G(ctx).Debug("resolving")
171		resp, err := fetcher.doRequestWithRetries(ctx, req, nil)
172		if err != nil {
173			if errors.Cause(err) == ErrInvalidAuthorization {
174				err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization")
175			}
176			return "", ocispec.Descriptor{}, err
177		}
178		resp.Body.Close() // don't care about body contents.
179
180		if resp.StatusCode > 299 {
181			if resp.StatusCode == http.StatusNotFound {
182				continue
183			}
184			return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
185		}
186
187		// this is the only point at which we trust the registry. we use the
188		// content headers to assemble a descriptor for the name. when this becomes
189		// more robust, we mostly get this information from a secure trust store.
190		dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
191
192		if dgstHeader != "" {
193			if err := dgstHeader.Validate(); err != nil {
194				return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
195			}
196			dgst = dgstHeader
197		}
198
199		if dgst == "" {
200			return "", ocispec.Descriptor{}, errors.Errorf("could not resolve digest for %v", ref)
201		}
202
203		var (
204			size       int64
205			sizeHeader = resp.Header.Get("Content-Length")
206		)
207
208		size, err = strconv.ParseInt(sizeHeader, 10, 64)
209		if err != nil {
210
211			return "", ocispec.Descriptor{}, errors.Wrapf(err, "invalid size header: %q", sizeHeader)
212		}
213		if size < 0 {
214			return "", ocispec.Descriptor{}, errors.Errorf("%q in header not a valid size", sizeHeader)
215		}
216
217		desc := ocispec.Descriptor{
218			Digest:    dgst,
219			MediaType: resp.Header.Get("Content-Type"), // need to strip disposition?
220			Size:      size,
221		}
222
223		log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
224		return ref, desc, nil
225	}
226
227	return "", ocispec.Descriptor{}, errors.Errorf("%v not found", ref)
228}
229
230func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
231	refspec, err := reference.Parse(ref)
232	if err != nil {
233		return nil, err
234	}
235
236	base, err := r.base(refspec)
237	if err != nil {
238		return nil, err
239	}
240
241	return dockerFetcher{
242		dockerBase: base,
243	}, nil
244}
245
246func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
247	refspec, err := reference.Parse(ref)
248	if err != nil {
249		return nil, err
250	}
251
252	// Manifests can be pushed by digest like any other object, but the passed in
253	// reference cannot take a digest without the associated content. A tag is allowed
254	// and will be used to tag pushed manifests.
255	if refspec.Object != "" && strings.Contains(refspec.Object, "@") {
256		return nil, errors.New("cannot use digest reference for push locator")
257	}
258
259	base, err := r.base(refspec)
260	if err != nil {
261		return nil, err
262	}
263
264	return dockerPusher{
265		dockerBase: base,
266		tag:        refspec.Object,
267		tracker:    r.tracker,
268	}, nil
269}
270
271type dockerBase struct {
272	refspec reference.Spec
273	base    url.URL
274
275	client           *http.Client
276	useBasic         bool
277	username, secret string
278	token            string
279	mu               sync.Mutex
280}
281
282func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
283	var (
284		err              error
285		base             url.URL
286		username, secret string
287	)
288
289	host := refspec.Hostname()
290	base.Host = host
291	if r.host != nil {
292		base.Host, err = r.host(host)
293		if err != nil {
294			return nil, err
295		}
296	}
297
298	base.Scheme = "https"
299	if r.plainHTTP || strings.HasPrefix(base.Host, "localhost:") {
300		base.Scheme = "http"
301	}
302
303	if r.credentials != nil {
304		username, secret, err = r.credentials(base.Host)
305		if err != nil {
306			return nil, err
307		}
308	}
309
310	prefix := strings.TrimPrefix(refspec.Locator, host+"/")
311	base.Path = path.Join("/v2", prefix)
312
313	return &dockerBase{
314		refspec:  refspec,
315		base:     base,
316		client:   r.client,
317		username: username,
318		secret:   secret,
319	}, nil
320}
321
322func (r *dockerBase) getToken() string {
323	r.mu.Lock()
324	defer r.mu.Unlock()
325
326	return r.token
327}
328
329func (r *dockerBase) setToken(token string) bool {
330	r.mu.Lock()
331	defer r.mu.Unlock()
332
333	changed := r.token != token
334	r.token = token
335
336	return changed
337}
338
339func (r *dockerBase) url(ps ...string) string {
340	url := r.base
341	url.Path = path.Join(url.Path, path.Join(ps...))
342	return url.String()
343}
344
345func (r *dockerBase) authorize(req *http.Request) {
346	token := r.getToken()
347	if r.useBasic {
348		req.SetBasicAuth(r.username, r.secret)
349	} else if token != "" {
350		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
351	}
352}
353
354func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
355	ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
356	log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request")
357	r.authorize(req)
358	resp, err := ctxhttp.Do(ctx, r.client, req)
359	if err != nil {
360		return nil, errors.Wrap(err, "failed to do request")
361	}
362	log.G(ctx).WithFields(logrus.Fields{
363		"status":           resp.Status,
364		"response.headers": resp.Header,
365	}).Debug("fetch response received")
366	return resp, nil
367}
368
369func (r *dockerBase) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
370	resp, err := r.doRequest(ctx, req)
371	if err != nil {
372		return nil, err
373	}
374
375	responses = append(responses, resp)
376	req, err = r.retryRequest(ctx, req, responses)
377	if err != nil {
378		resp.Body.Close()
379		return nil, err
380	}
381	if req != nil {
382		resp.Body.Close()
383		return r.doRequestWithRetries(ctx, req, responses)
384	}
385	return resp, err
386}
387
388func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
389	if len(responses) > 5 {
390		return nil, nil
391	}
392	last := responses[len(responses)-1]
393	if last.StatusCode == http.StatusUnauthorized {
394		log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
395		for _, c := range parseAuthHeader(last.Header) {
396			if c.scheme == bearerAuth {
397				if err := invalidAuthorization(c, responses); err != nil {
398					r.setToken("")
399					return nil, err
400				}
401				if err := r.setTokenAuth(ctx, c.parameters); err != nil {
402					return nil, err
403				}
404				return copyRequest(req)
405			} else if c.scheme == basicAuth {
406				if r.username != "" && r.secret != "" {
407					r.useBasic = true
408				}
409				return copyRequest(req)
410			}
411		}
412		return nil, nil
413	} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
414		// Support registries which have not properly implemented the HEAD method for
415		// manifests endpoint
416		if strings.Contains(req.URL.Path, "/manifests/") {
417			// TODO: copy request?
418			req.Method = http.MethodGet
419			return copyRequest(req)
420		}
421	}
422
423	// TODO: Handle 50x errors accounting for attempt history
424	return nil, nil
425}
426
427func invalidAuthorization(c challenge, responses []*http.Response) error {
428	errStr := c.parameters["error"]
429	if errStr == "" {
430		return nil
431	}
432
433	n := len(responses)
434	if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
435		return nil
436	}
437
438	return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
439}
440
441func sameRequest(r1, r2 *http.Request) bool {
442	if r1.Method != r2.Method {
443		return false
444	}
445	if *r1.URL != *r2.URL {
446		return false
447	}
448	return true
449}
450
451func copyRequest(req *http.Request) (*http.Request, error) {
452	ireq := *req
453	if ireq.GetBody != nil {
454		var err error
455		ireq.Body, err = ireq.GetBody()
456		if err != nil {
457			return nil, err
458		}
459	}
460	return &ireq, nil
461}
462
463func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) error {
464	realm, ok := params["realm"]
465	if !ok {
466		return errors.New("no realm specified for token auth challenge")
467	}
468
469	realmURL, err := url.Parse(realm)
470	if err != nil {
471		return fmt.Errorf("invalid token auth challenge realm: %s", err)
472	}
473
474	to := tokenOptions{
475		realm:   realmURL.String(),
476		service: params["service"],
477	}
478
479	to.scopes = getTokenScopes(ctx, params)
480	if len(to.scopes) == 0 {
481		return errors.Errorf("no scope specified for token auth challenge")
482	}
483
484	var token string
485	if r.secret != "" {
486		// Credential information is provided, use oauth POST endpoint
487		token, err = r.fetchTokenWithOAuth(ctx, to)
488		if err != nil {
489			return errors.Wrap(err, "failed to fetch oauth token")
490		}
491	} else {
492		// Do request anonymously
493		token, err = r.fetchToken(ctx, to)
494		if err != nil {
495			return errors.Wrap(err, "failed to fetch anonymous token")
496		}
497	}
498	r.setToken(token)
499
500	return nil
501}
502
503type tokenOptions struct {
504	realm   string
505	service string
506	scopes  []string
507}
508
509type postTokenResponse struct {
510	AccessToken  string    `json:"access_token"`
511	RefreshToken string    `json:"refresh_token"`
512	ExpiresIn    int       `json:"expires_in"`
513	IssuedAt     time.Time `json:"issued_at"`
514	Scope        string    `json:"scope"`
515}
516
517func (r *dockerBase) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
518	form := url.Values{}
519	form.Set("scope", strings.Join(to.scopes, " "))
520	form.Set("service", to.service)
521	// TODO: Allow setting client_id
522	form.Set("client_id", "containerd-dist-tool")
523
524	if r.username == "" {
525		form.Set("grant_type", "refresh_token")
526		form.Set("refresh_token", r.secret)
527	} else {
528		form.Set("grant_type", "password")
529		form.Set("username", r.username)
530		form.Set("password", r.secret)
531	}
532
533	resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form)
534	if err != nil {
535		return "", err
536	}
537	defer resp.Body.Close()
538
539	// Registries without support for POST may return 404 for POST /v2/token.
540	// As of September 2017, GCR is known to return 404.
541	// As of February 2018, JFrog Artifactory is known to return 401.
542	if (resp.StatusCode == 405 && r.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
543		return r.fetchToken(ctx, to)
544	} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
545		b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
546		log.G(ctx).WithFields(logrus.Fields{
547			"status": resp.Status,
548			"body":   string(b),
549		}).Debugf("token request failed")
550		// TODO: handle error body and write debug output
551		return "", errors.Errorf("unexpected status: %s", resp.Status)
552	}
553
554	decoder := json.NewDecoder(resp.Body)
555
556	var tr postTokenResponse
557	if err = decoder.Decode(&tr); err != nil {
558		return "", fmt.Errorf("unable to decode token response: %s", err)
559	}
560
561	return tr.AccessToken, nil
562}
563
564type getTokenResponse struct {
565	Token        string    `json:"token"`
566	AccessToken  string    `json:"access_token"`
567	ExpiresIn    int       `json:"expires_in"`
568	IssuedAt     time.Time `json:"issued_at"`
569	RefreshToken string    `json:"refresh_token"`
570}
571
572// getToken fetches a token using a GET request
573func (r *dockerBase) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
574	req, err := http.NewRequest("GET", to.realm, nil)
575	if err != nil {
576		return "", err
577	}
578
579	reqParams := req.URL.Query()
580
581	if to.service != "" {
582		reqParams.Add("service", to.service)
583	}
584
585	for _, scope := range to.scopes {
586		reqParams.Add("scope", scope)
587	}
588
589	if r.secret != "" {
590		req.SetBasicAuth(r.username, r.secret)
591	}
592
593	req.URL.RawQuery = reqParams.Encode()
594
595	resp, err := ctxhttp.Do(ctx, r.client, req)
596	if err != nil {
597		return "", err
598	}
599	defer resp.Body.Close()
600
601	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
602		// TODO: handle error body and write debug output
603		return "", errors.Errorf("unexpected status: %s", resp.Status)
604	}
605
606	decoder := json.NewDecoder(resp.Body)
607
608	var tr getTokenResponse
609	if err = decoder.Decode(&tr); err != nil {
610		return "", fmt.Errorf("unable to decode token response: %s", err)
611	}
612
613	// `access_token` is equivalent to `token` and if both are specified
614	// the choice is undefined.  Canonicalize `access_token` by sticking
615	// things in `token`.
616	if tr.AccessToken != "" {
617		tr.Token = tr.AccessToken
618	}
619
620	if tr.Token == "" {
621		return "", ErrNoToken
622	}
623
624	return tr.Token, nil
625}
626