123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- /*
- 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 {
- to.scopes = append(to.scopes, scope)
- } else {
- log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge")
- }
- 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)
- // 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{}
- if len(to.scopes) > 0 {
- 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
- }
|