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/base64"
22	"fmt"
23	"net/http"
24	"strings"
25	"sync"
26
27	"github.com/containerd/containerd/errdefs"
28	"github.com/containerd/containerd/log"
29	"github.com/containerd/containerd/remotes/docker/auth"
30	remoteerrors "github.com/containerd/containerd/remotes/errors"
31	"github.com/pkg/errors"
32	"github.com/sirupsen/logrus"
33)
34
35type dockerAuthorizer struct {
36	credentials func(string) (string, string, error)
37
38	client *http.Client
39	header http.Header
40	mu     sync.Mutex
41
42	// indexed by host name
43	handlers map[string]*authHandler
44}
45
46// NewAuthorizer creates a Docker authorizer using the provided function to
47// get credentials for the token server or basic auth.
48// Deprecated: Use NewDockerAuthorizer
49func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
50	return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f))
51}
52
53type authorizerConfig struct {
54	credentials func(string) (string, string, error)
55	client      *http.Client
56	header      http.Header
57}
58
59// AuthorizerOpt configures an authorizer
60type AuthorizerOpt func(*authorizerConfig)
61
62// WithAuthClient provides the HTTP client for the authorizer
63func WithAuthClient(client *http.Client) AuthorizerOpt {
64	return func(opt *authorizerConfig) {
65		opt.client = client
66	}
67}
68
69// WithAuthCreds provides a credential function to the authorizer
70func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt {
71	return func(opt *authorizerConfig) {
72		opt.credentials = creds
73	}
74}
75
76// WithAuthHeader provides HTTP headers for authorization
77func WithAuthHeader(hdr http.Header) AuthorizerOpt {
78	return func(opt *authorizerConfig) {
79		opt.header = hdr
80	}
81}
82
83// NewDockerAuthorizer creates an authorizer using Docker's registry
84// authentication spec.
85// See https://docs.docker.com/registry/spec/auth/
86func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
87	var ao authorizerConfig
88	for _, opt := range opts {
89		opt(&ao)
90	}
91
92	if ao.client == nil {
93		ao.client = http.DefaultClient
94	}
95
96	return &dockerAuthorizer{
97		credentials: ao.credentials,
98		client:      ao.client,
99		header:      ao.header,
100		handlers:    make(map[string]*authHandler),
101	}
102}
103
104// Authorize handles auth request.
105func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
106	// skip if there is no auth handler
107	ah := a.getAuthHandler(req.URL.Host)
108	if ah == nil {
109		return nil
110	}
111
112	auth, err := ah.authorize(ctx)
113	if err != nil {
114		return err
115	}
116
117	req.Header.Set("Authorization", auth)
118	return nil
119}
120
121func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler {
122	a.mu.Lock()
123	defer a.mu.Unlock()
124
125	return a.handlers[host]
126}
127
128func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
129	last := responses[len(responses)-1]
130	host := last.Request.URL.Host
131
132	a.mu.Lock()
133	defer a.mu.Unlock()
134	for _, c := range auth.ParseAuthHeader(last.Header) {
135		if c.Scheme == auth.BearerAuth {
136			if err := invalidAuthorization(c, responses); err != nil {
137				delete(a.handlers, host)
138				return err
139			}
140
141			// reuse existing handler
142			//
143			// assume that one registry will return the common
144			// challenge information, including realm and service.
145			// and the resource scope is only different part
146			// which can be provided by each request.
147			if _, ok := a.handlers[host]; ok {
148				return nil
149			}
150
151			var username, secret string
152			if a.credentials != nil {
153				var err error
154				username, secret, err = a.credentials(host)
155				if err != nil {
156					return err
157				}
158			}
159
160			common, err := auth.GenerateTokenOptions(ctx, host, username, secret, c)
161			if err != nil {
162				return err
163			}
164
165			a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, common)
166			return nil
167		} else if c.Scheme == auth.BasicAuth && a.credentials != nil {
168			username, secret, err := a.credentials(host)
169			if err != nil {
170				return err
171			}
172
173			if username != "" && secret != "" {
174				common := auth.TokenOptions{
175					Username: username,
176					Secret:   secret,
177				}
178
179				a.handlers[host] = newAuthHandler(a.client, a.header, c.Scheme, common)
180				return nil
181			}
182		}
183	}
184	return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
185}
186
187// authResult is used to control limit rate.
188type authResult struct {
189	sync.WaitGroup
190	token string
191	err   error
192}
193
194// authHandler is used to handle auth request per registry server.
195type authHandler struct {
196	sync.Mutex
197
198	header http.Header
199
200	client *http.Client
201
202	// only support basic and bearer schemes
203	scheme auth.AuthenticationScheme
204
205	// common contains common challenge answer
206	common auth.TokenOptions
207
208	// scopedTokens caches token indexed by scopes, which used in
209	// bearer auth case
210	scopedTokens map[string]*authResult
211}
212
213func newAuthHandler(client *http.Client, hdr http.Header, scheme auth.AuthenticationScheme, opts auth.TokenOptions) *authHandler {
214	return &authHandler{
215		header:       hdr,
216		client:       client,
217		scheme:       scheme,
218		common:       opts,
219		scopedTokens: map[string]*authResult{},
220	}
221}
222
223func (ah *authHandler) authorize(ctx context.Context) (string, error) {
224	switch ah.scheme {
225	case auth.BasicAuth:
226		return ah.doBasicAuth(ctx)
227	case auth.BearerAuth:
228		return ah.doBearerAuth(ctx)
229	default:
230		return "", errors.Wrapf(errdefs.ErrNotImplemented, "failed to find supported auth scheme: %s", string(ah.scheme))
231	}
232}
233
234func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
235	username, secret := ah.common.Username, ah.common.Secret
236
237	if username == "" || secret == "" {
238		return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
239	}
240
241	auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
242	return fmt.Sprintf("Basic %s", auth), nil
243}
244
245func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err error) {
246	// copy common tokenOptions
247	to := ah.common
248
249	to.Scopes = GetTokenScopes(ctx, to.Scopes)
250
251	// Docs: https://docs.docker.com/registry/spec/auth/scope
252	scoped := strings.Join(to.Scopes, " ")
253
254	ah.Lock()
255	if r, exist := ah.scopedTokens[scoped]; exist {
256		ah.Unlock()
257		r.Wait()
258		return r.token, r.err
259	}
260
261	// only one fetch token job
262	r := new(authResult)
263	r.Add(1)
264	ah.scopedTokens[scoped] = r
265	ah.Unlock()
266
267	defer func() {
268		token = fmt.Sprintf("Bearer %s", token)
269		r.token, r.err = token, err
270		r.Done()
271	}()
272
273	// fetch token for the resource scope
274	if to.Secret != "" {
275		defer func() {
276			err = errors.Wrap(err, "failed to fetch oauth token")
277		}()
278		// credential information is provided, use oauth POST endpoint
279		// TODO: Allow setting client_id
280		resp, err := auth.FetchTokenWithOAuth(ctx, ah.client, ah.header, "containerd-client", to)
281		if err != nil {
282			var errStatus remoteerrors.ErrUnexpectedStatus
283			if errors.As(err, &errStatus) {
284				// Registries without support for POST may return 404 for POST /v2/token.
285				// As of September 2017, GCR is known to return 404.
286				// As of February 2018, JFrog Artifactory is known to return 401.
287				if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
288					resp, err := auth.FetchToken(ctx, ah.client, ah.header, to)
289					if err != nil {
290						return "", err
291					}
292					return resp.Token, nil
293				}
294				log.G(ctx).WithFields(logrus.Fields{
295					"status": errStatus.Status,
296					"body":   string(errStatus.Body),
297				}).Debugf("token request failed")
298			}
299			return "", err
300		}
301		return resp.AccessToken, nil
302	}
303	// do request anonymously
304	resp, err := auth.FetchToken(ctx, ah.client, ah.header, to)
305	if err != nil {
306		return "", errors.Wrap(err, "failed to fetch anonymous token")
307	}
308	return resp.Token, nil
309}
310
311func invalidAuthorization(c auth.Challenge, responses []*http.Response) error {
312	errStr := c.Parameters["error"]
313	if errStr == "" {
314		return nil
315	}
316
317	n := len(responses)
318	if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
319		return nil
320	}
321
322	return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
323}
324
325func sameRequest(r1, r2 *http.Request) bool {
326	if r1.Method != r2.Method {
327		return false
328	}
329	if *r1.URL != *r2.URL {
330		return false
331	}
332	return true
333}
334