483 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			483 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   Copyright The containerd Authors.
 | 
						|
 | 
						|
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
   you may not use this file except in compliance with the License.
 | 
						|
   You may obtain a copy of the License at
 | 
						|
 | 
						|
       http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
   Unless required by applicable law or agreed to in writing, software
 | 
						|
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
   See the License for the specific language governing permissions and
 | 
						|
   limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package docker
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"encoding/base64"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"io/ioutil"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/containerd/containerd/errdefs"
 | 
						|
	"github.com/containerd/containerd/log"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"github.com/sirupsen/logrus"
 | 
						|
	"golang.org/x/net/context/ctxhttp"
 | 
						|
)
 | 
						|
 | 
						|
type dockerAuthorizer struct {
 | 
						|
	credentials func(string) (string, string, error)
 | 
						|
 | 
						|
	client *http.Client
 | 
						|
	header http.Header
 | 
						|
	mu     sync.Mutex
 | 
						|
 | 
						|
	// indexed by host name
 | 
						|
	handlers map[string]*authHandler
 | 
						|
}
 | 
						|
 | 
						|
// NewAuthorizer creates a Docker authorizer using the provided function to
 | 
						|
// get credentials for the token server or basic auth.
 | 
						|
// Deprecated: Use NewDockerAuthorizer
 | 
						|
func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
 | 
						|
	return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f))
 | 
						|
}
 | 
						|
 | 
						|
type authorizerConfig struct {
 | 
						|
	credentials func(string) (string, string, error)
 | 
						|
	client      *http.Client
 | 
						|
	header      http.Header
 | 
						|
}
 | 
						|
 | 
						|
// AuthorizerOpt configures an authorizer
 | 
						|
type AuthorizerOpt func(*authorizerConfig)
 | 
						|
 | 
						|
// WithAuthClient provides the HTTP client for the authorizer
 | 
						|
func WithAuthClient(client *http.Client) AuthorizerOpt {
 | 
						|
	return func(opt *authorizerConfig) {
 | 
						|
		opt.client = client
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithAuthCreds provides a credential function to the authorizer
 | 
						|
func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt {
 | 
						|
	return func(opt *authorizerConfig) {
 | 
						|
		opt.credentials = creds
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// WithAuthHeader provides HTTP headers for authorization
 | 
						|
func WithAuthHeader(hdr http.Header) AuthorizerOpt {
 | 
						|
	return func(opt *authorizerConfig) {
 | 
						|
		opt.header = hdr
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// NewDockerAuthorizer creates an authorizer using Docker's registry
 | 
						|
// authentication spec.
 | 
						|
// See https://docs.docker.com/registry/spec/auth/
 | 
						|
func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
 | 
						|
	var ao authorizerConfig
 | 
						|
	for _, opt := range opts {
 | 
						|
		opt(&ao)
 | 
						|
	}
 | 
						|
 | 
						|
	if ao.client == nil {
 | 
						|
		ao.client = http.DefaultClient
 | 
						|
	}
 | 
						|
 | 
						|
	return &dockerAuthorizer{
 | 
						|
		credentials: ao.credentials,
 | 
						|
		client:      ao.client,
 | 
						|
		header:      ao.header,
 | 
						|
		handlers:    make(map[string]*authHandler),
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Authorize handles auth request.
 | 
						|
func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
 | 
						|
	// skip if there is no auth handler
 | 
						|
	ah := a.getAuthHandler(req.URL.Host)
 | 
						|
	if ah == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	auth, err := ah.authorize(ctx)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	req.Header.Set("Authorization", auth)
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler {
 | 
						|
	a.mu.Lock()
 | 
						|
	defer a.mu.Unlock()
 | 
						|
 | 
						|
	return a.handlers[host]
 | 
						|
}
 | 
						|
 | 
						|
func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
 | 
						|
	last := responses[len(responses)-1]
 | 
						|
	host := last.Request.URL.Host
 | 
						|
 | 
						|
	a.mu.Lock()
 | 
						|
	defer a.mu.Unlock()
 | 
						|
	for _, c := range parseAuthHeader(last.Header) {
 | 
						|
		if c.scheme == bearerAuth {
 | 
						|
			if err := invalidAuthorization(c, responses); err != nil {
 | 
						|
				delete(a.handlers, host)
 | 
						|
				return err
 | 
						|
			}
 | 
						|
 | 
						|
			// reuse existing handler
 | 
						|
			//
 | 
						|
			// assume that one registry will return the common
 | 
						|
			// challenge information, including realm and service.
 | 
						|
			// and the resource scope is only different part
 | 
						|
			// which can be provided by each request.
 | 
						|
			if _, ok := a.handlers[host]; ok {
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
 | 
						|
			common, err := a.generateTokenOptions(ctx, host, c)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
 | 
						|
			a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
 | 
						|
			return nil
 | 
						|
		} else if c.scheme == basicAuth && a.credentials != nil {
 | 
						|
			username, secret, err := a.credentials(host)
 | 
						|
			if err != nil {
 | 
						|
				return err
 | 
						|
			}
 | 
						|
 | 
						|
			if username != "" && secret != "" {
 | 
						|
				common := tokenOptions{
 | 
						|
					username: username,
 | 
						|
					secret:   secret,
 | 
						|
				}
 | 
						|
 | 
						|
				a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
 | 
						|
}
 | 
						|
 | 
						|
func (a *dockerAuthorizer) generateTokenOptions(ctx context.Context, host string, c challenge) (tokenOptions, error) {
 | 
						|
	realm, ok := c.parameters["realm"]
 | 
						|
	if !ok {
 | 
						|
		return tokenOptions{}, errors.New("no realm specified for token auth challenge")
 | 
						|
	}
 | 
						|
 | 
						|
	realmURL, err := url.Parse(realm)
 | 
						|
	if err != nil {
 | 
						|
		return tokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
 | 
						|
	}
 | 
						|
 | 
						|
	to := tokenOptions{
 | 
						|
		realm:   realmURL.String(),
 | 
						|
		service: c.parameters["service"],
 | 
						|
	}
 | 
						|
 | 
						|
	scope, ok := c.parameters["scope"]
 | 
						|
	if !ok {
 | 
						|
		return tokenOptions{}, errors.Errorf("no scope specified for token auth challenge")
 | 
						|
	}
 | 
						|
	to.scopes = append(to.scopes, scope)
 | 
						|
 | 
						|
	if a.credentials != nil {
 | 
						|
		to.username, to.secret, err = a.credentials(host)
 | 
						|
		if err != nil {
 | 
						|
			return tokenOptions{}, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return to, nil
 | 
						|
}
 | 
						|
 | 
						|
// authResult is used to control limit rate.
 | 
						|
type authResult struct {
 | 
						|
	sync.WaitGroup
 | 
						|
	token string
 | 
						|
	err   error
 | 
						|
}
 | 
						|
 | 
						|
// authHandler is used to handle auth request per registry server.
 | 
						|
type authHandler struct {
 | 
						|
	sync.Mutex
 | 
						|
 | 
						|
	header http.Header
 | 
						|
 | 
						|
	client *http.Client
 | 
						|
 | 
						|
	// only support basic and bearer schemes
 | 
						|
	scheme authenticationScheme
 | 
						|
 | 
						|
	// common contains common challenge answer
 | 
						|
	common tokenOptions
 | 
						|
 | 
						|
	// scopedTokens caches token indexed by scopes, which used in
 | 
						|
	// bearer auth case
 | 
						|
	scopedTokens map[string]*authResult
 | 
						|
}
 | 
						|
 | 
						|
func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler {
 | 
						|
	return &authHandler{
 | 
						|
		header:       hdr,
 | 
						|
		client:       client,
 | 
						|
		scheme:       scheme,
 | 
						|
		common:       opts,
 | 
						|
		scopedTokens: map[string]*authResult{},
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (ah *authHandler) authorize(ctx context.Context) (string, error) {
 | 
						|
	switch ah.scheme {
 | 
						|
	case basicAuth:
 | 
						|
		return ah.doBasicAuth(ctx)
 | 
						|
	case bearerAuth:
 | 
						|
		return ah.doBearerAuth(ctx)
 | 
						|
	default:
 | 
						|
		return "", errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
 | 
						|
	username, secret := ah.common.username, ah.common.secret
 | 
						|
 | 
						|
	if username == "" || secret == "" {
 | 
						|
		return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
 | 
						|
	}
 | 
						|
 | 
						|
	auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
 | 
						|
	return fmt.Sprintf("Basic %s", auth), nil
 | 
						|
}
 | 
						|
 | 
						|
func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) {
 | 
						|
	// copy common tokenOptions
 | 
						|
	to := ah.common
 | 
						|
 | 
						|
	to.scopes = getTokenScopes(ctx, to.scopes)
 | 
						|
	if len(to.scopes) == 0 {
 | 
						|
		return "", errors.Errorf("no scope specified for token auth challenge")
 | 
						|
	}
 | 
						|
 | 
						|
	// Docs: https://docs.docker.com/registry/spec/auth/scope
 | 
						|
	scoped := strings.Join(to.scopes, " ")
 | 
						|
 | 
						|
	ah.Lock()
 | 
						|
	if r, exist := ah.scopedTokens[scoped]; exist {
 | 
						|
		ah.Unlock()
 | 
						|
		r.Wait()
 | 
						|
		return r.token, r.err
 | 
						|
	}
 | 
						|
 | 
						|
	// only one fetch token job
 | 
						|
	r := new(authResult)
 | 
						|
	r.Add(1)
 | 
						|
	ah.scopedTokens[scoped] = r
 | 
						|
	ah.Unlock()
 | 
						|
 | 
						|
	// fetch token for the resource scope
 | 
						|
	var (
 | 
						|
		token string
 | 
						|
		err   error
 | 
						|
	)
 | 
						|
	if to.secret != "" {
 | 
						|
		// credential information is provided, use oauth POST endpoint
 | 
						|
		token, err = ah.fetchTokenWithOAuth(ctx, to)
 | 
						|
		err = errors.Wrap(err, "failed to fetch oauth token")
 | 
						|
	} else {
 | 
						|
		// do request anonymously
 | 
						|
		token, err = ah.fetchToken(ctx, to)
 | 
						|
		err = errors.Wrap(err, "failed to fetch anonymous token")
 | 
						|
	}
 | 
						|
	token = fmt.Sprintf("Bearer %s", token)
 | 
						|
 | 
						|
	r.token, r.err = token, err
 | 
						|
	r.Done()
 | 
						|
	return r.token, r.err
 | 
						|
}
 | 
						|
 | 
						|
type tokenOptions struct {
 | 
						|
	realm    string
 | 
						|
	service  string
 | 
						|
	scopes   []string
 | 
						|
	username string
 | 
						|
	secret   string
 | 
						|
}
 | 
						|
 | 
						|
type postTokenResponse struct {
 | 
						|
	AccessToken  string    `json:"access_token"`
 | 
						|
	RefreshToken string    `json:"refresh_token"`
 | 
						|
	ExpiresIn    int       `json:"expires_in"`
 | 
						|
	IssuedAt     time.Time `json:"issued_at"`
 | 
						|
	Scope        string    `json:"scope"`
 | 
						|
}
 | 
						|
 | 
						|
func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
 | 
						|
	form := url.Values{}
 | 
						|
	form.Set("scope", strings.Join(to.scopes, " "))
 | 
						|
	form.Set("service", to.service)
 | 
						|
	// TODO: Allow setting client_id
 | 
						|
	form.Set("client_id", "containerd-client")
 | 
						|
 | 
						|
	if to.username == "" {
 | 
						|
		form.Set("grant_type", "refresh_token")
 | 
						|
		form.Set("refresh_token", to.secret)
 | 
						|
	} else {
 | 
						|
		form.Set("grant_type", "password")
 | 
						|
		form.Set("username", to.username)
 | 
						|
		form.Set("password", to.secret)
 | 
						|
	}
 | 
						|
 | 
						|
	req, err := http.NewRequest("POST", to.realm, strings.NewReader(form.Encode()))
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
 | 
						|
	if ah.header != nil {
 | 
						|
		for k, v := range ah.header {
 | 
						|
			req.Header[k] = append(req.Header[k], v...)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := ctxhttp.Do(ctx, ah.client, req)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	defer resp.Body.Close()
 | 
						|
 | 
						|
	// Registries without support for POST may return 404 for POST /v2/token.
 | 
						|
	// As of September 2017, GCR is known to return 404.
 | 
						|
	// As of February 2018, JFrog Artifactory is known to return 401.
 | 
						|
	if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
 | 
						|
		return ah.fetchToken(ctx, to)
 | 
						|
	} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
 | 
						|
		b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
 | 
						|
		log.G(ctx).WithFields(logrus.Fields{
 | 
						|
			"status": resp.Status,
 | 
						|
			"body":   string(b),
 | 
						|
		}).Debugf("token request failed")
 | 
						|
		// TODO: handle error body and write debug output
 | 
						|
		return "", errors.Errorf("unexpected status: %s", resp.Status)
 | 
						|
	}
 | 
						|
 | 
						|
	decoder := json.NewDecoder(resp.Body)
 | 
						|
 | 
						|
	var tr postTokenResponse
 | 
						|
	if err = decoder.Decode(&tr); err != nil {
 | 
						|
		return "", fmt.Errorf("unable to decode token response: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	return tr.AccessToken, nil
 | 
						|
}
 | 
						|
 | 
						|
type getTokenResponse struct {
 | 
						|
	Token        string    `json:"token"`
 | 
						|
	AccessToken  string    `json:"access_token"`
 | 
						|
	ExpiresIn    int       `json:"expires_in"`
 | 
						|
	IssuedAt     time.Time `json:"issued_at"`
 | 
						|
	RefreshToken string    `json:"refresh_token"`
 | 
						|
}
 | 
						|
 | 
						|
// fetchToken fetches a token using a GET request
 | 
						|
func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
 | 
						|
	req, err := http.NewRequest("GET", to.realm, nil)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	if ah.header != nil {
 | 
						|
		for k, v := range ah.header {
 | 
						|
			req.Header[k] = append(req.Header[k], v...)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	reqParams := req.URL.Query()
 | 
						|
 | 
						|
	if to.service != "" {
 | 
						|
		reqParams.Add("service", to.service)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, scope := range to.scopes {
 | 
						|
		reqParams.Add("scope", scope)
 | 
						|
	}
 | 
						|
 | 
						|
	if to.secret != "" {
 | 
						|
		req.SetBasicAuth(to.username, to.secret)
 | 
						|
	}
 | 
						|
 | 
						|
	req.URL.RawQuery = reqParams.Encode()
 | 
						|
 | 
						|
	resp, err := ctxhttp.Do(ctx, ah.client, req)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	defer resp.Body.Close()
 | 
						|
 | 
						|
	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
 | 
						|
		// TODO: handle error body and write debug output
 | 
						|
		return "", errors.Errorf("unexpected status: %s", resp.Status)
 | 
						|
	}
 | 
						|
 | 
						|
	decoder := json.NewDecoder(resp.Body)
 | 
						|
 | 
						|
	var tr getTokenResponse
 | 
						|
	if err = decoder.Decode(&tr); err != nil {
 | 
						|
		return "", fmt.Errorf("unable to decode token response: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	// `access_token` is equivalent to `token` and if both are specified
 | 
						|
	// the choice is undefined.  Canonicalize `access_token` by sticking
 | 
						|
	// things in `token`.
 | 
						|
	if tr.AccessToken != "" {
 | 
						|
		tr.Token = tr.AccessToken
 | 
						|
	}
 | 
						|
 | 
						|
	if tr.Token == "" {
 | 
						|
		return "", ErrNoToken
 | 
						|
	}
 | 
						|
 | 
						|
	return tr.Token, nil
 | 
						|
}
 | 
						|
 | 
						|
func invalidAuthorization(c challenge, responses []*http.Response) error {
 | 
						|
	errStr := c.parameters["error"]
 | 
						|
	if errStr == "" {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	n := len(responses)
 | 
						|
	if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
 | 
						|
}
 | 
						|
 | 
						|
func sameRequest(r1, r2 *http.Request) bool {
 | 
						|
	if r1.Method != r2.Method {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if *r1.URL != *r2.URL {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	return true
 | 
						|
}
 |