|
@@ -18,18 +18,13 @@ package docker
|
|
|
|
|
|
import (
|
|
import (
|
|
"context"
|
|
"context"
|
|
- "encoding/json"
|
|
|
|
- "fmt"
|
|
|
|
- "io"
|
|
|
|
- "io/ioutil"
|
|
|
|
"net/http"
|
|
"net/http"
|
|
"net/url"
|
|
"net/url"
|
|
"path"
|
|
"path"
|
|
"strconv"
|
|
"strconv"
|
|
"strings"
|
|
"strings"
|
|
- "sync"
|
|
|
|
- "time"
|
|
|
|
|
|
|
|
|
|
+ "github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/images"
|
|
"github.com/containerd/containerd/images"
|
|
"github.com/containerd/containerd/log"
|
|
"github.com/containerd/containerd/log"
|
|
"github.com/containerd/containerd/reference"
|
|
"github.com/containerd/containerd/reference"
|
|
@@ -51,19 +46,37 @@ var (
|
|
ErrInvalidAuthorization = errors.New("authorization failed")
|
|
ErrInvalidAuthorization = errors.New("authorization failed")
|
|
)
|
|
)
|
|
|
|
|
|
-type dockerResolver struct {
|
|
|
|
- credentials func(string) (string, string, error)
|
|
|
|
- host func(string) (string, error)
|
|
|
|
- plainHTTP bool
|
|
|
|
- client *http.Client
|
|
|
|
- tracker StatusTracker
|
|
|
|
|
|
+// Authorizer is used to authorize HTTP requests based on 401 HTTP responses.
|
|
|
|
+// An Authorizer is responsible for caching tokens or credentials used by
|
|
|
|
+// requests.
|
|
|
|
+type Authorizer interface {
|
|
|
|
+ // Authorize sets the appropriate `Authorization` header on the given
|
|
|
|
+ // request.
|
|
|
|
+ //
|
|
|
|
+ // If no authorization is found for the request, the request remains
|
|
|
|
+ // unmodified. It may also add an `Authorization` header as
|
|
|
|
+ // "bearer <some bearer token>"
|
|
|
|
+ // "basic <base64 encoded credentials>"
|
|
|
|
+ Authorize(context.Context, *http.Request) error
|
|
|
|
+
|
|
|
|
+ // AddResponses adds a 401 response for the authorizer to consider when
|
|
|
|
+ // authorizing requests. The last response should be unauthorized and
|
|
|
|
+ // the previous requests are used to consider redirects and retries
|
|
|
|
+ // that may have led to the 401.
|
|
|
|
+ //
|
|
|
|
+ // If response is not handled, returns `ErrNotImplemented`
|
|
|
|
+ AddResponses(context.Context, []*http.Response) error
|
|
}
|
|
}
|
|
|
|
|
|
// ResolverOptions are used to configured a new Docker register resolver
|
|
// ResolverOptions are used to configured a new Docker register resolver
|
|
type ResolverOptions struct {
|
|
type ResolverOptions struct {
|
|
|
|
+ // Authorizer is used to authorize registry requests
|
|
|
|
+ Authorizer Authorizer
|
|
|
|
+
|
|
// Credentials provides username and secret given a host.
|
|
// Credentials provides username and secret given a host.
|
|
// If username is empty but a secret is given, that secret
|
|
// If username is empty but a secret is given, that secret
|
|
// is interpretted as a long lived token.
|
|
// is interpretted as a long lived token.
|
|
|
|
+ // Deprecated: use Authorizer
|
|
Credentials func(string) (string, string, error)
|
|
Credentials func(string) (string, string, error)
|
|
|
|
|
|
// Host provides the hostname given a namespace.
|
|
// Host provides the hostname given a namespace.
|
|
@@ -89,22 +102,31 @@ func DefaultHost(ns string) (string, error) {
|
|
return ns, nil
|
|
return ns, nil
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+type dockerResolver struct {
|
|
|
|
+ auth Authorizer
|
|
|
|
+ host func(string) (string, error)
|
|
|
|
+ plainHTTP bool
|
|
|
|
+ client *http.Client
|
|
|
|
+ tracker StatusTracker
|
|
|
|
+}
|
|
|
|
+
|
|
// NewResolver returns a new resolver to a Docker registry
|
|
// NewResolver returns a new resolver to a Docker registry
|
|
func NewResolver(options ResolverOptions) remotes.Resolver {
|
|
func NewResolver(options ResolverOptions) remotes.Resolver {
|
|
- tracker := options.Tracker
|
|
|
|
- if tracker == nil {
|
|
|
|
- tracker = NewInMemoryTracker()
|
|
|
|
|
|
+ if options.Tracker == nil {
|
|
|
|
+ options.Tracker = NewInMemoryTracker()
|
|
|
|
+ }
|
|
|
|
+ if options.Host == nil {
|
|
|
|
+ options.Host = DefaultHost
|
|
}
|
|
}
|
|
- host := options.Host
|
|
|
|
- if host == nil {
|
|
|
|
- host = DefaultHost
|
|
|
|
|
|
+ if options.Authorizer == nil {
|
|
|
|
+ options.Authorizer = NewAuthorizer(options.Client, options.Credentials)
|
|
}
|
|
}
|
|
return &dockerResolver{
|
|
return &dockerResolver{
|
|
- credentials: options.Credentials,
|
|
|
|
- host: host,
|
|
|
|
- plainHTTP: options.PlainHTTP,
|
|
|
|
- client: options.Client,
|
|
|
|
- tracker: tracker,
|
|
|
|
|
|
+ auth: options.Authorizer,
|
|
|
|
+ host: options.Host,
|
|
|
|
+ plainHTTP: options.PlainHTTP,
|
|
|
|
+ client: options.Client,
|
|
|
|
+ tracker: options.Tracker,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -272,18 +294,14 @@ type dockerBase struct {
|
|
refspec reference.Spec
|
|
refspec reference.Spec
|
|
base url.URL
|
|
base url.URL
|
|
|
|
|
|
- client *http.Client
|
|
|
|
- useBasic bool
|
|
|
|
- username, secret string
|
|
|
|
- token string
|
|
|
|
- mu sync.Mutex
|
|
|
|
|
|
+ client *http.Client
|
|
|
|
+ auth Authorizer
|
|
}
|
|
}
|
|
|
|
|
|
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
|
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
|
var (
|
|
var (
|
|
- err error
|
|
|
|
- base url.URL
|
|
|
|
- username, secret string
|
|
|
|
|
|
+ err error
|
|
|
|
+ base url.URL
|
|
)
|
|
)
|
|
|
|
|
|
host := refspec.Hostname()
|
|
host := refspec.Hostname()
|
|
@@ -300,61 +318,40 @@ func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
|
base.Scheme = "http"
|
|
base.Scheme = "http"
|
|
}
|
|
}
|
|
|
|
|
|
- if r.credentials != nil {
|
|
|
|
- username, secret, err = r.credentials(base.Host)
|
|
|
|
- if err != nil {
|
|
|
|
- return nil, err
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
|
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
|
base.Path = path.Join("/v2", prefix)
|
|
base.Path = path.Join("/v2", prefix)
|
|
|
|
|
|
return &dockerBase{
|
|
return &dockerBase{
|
|
- refspec: refspec,
|
|
|
|
- base: base,
|
|
|
|
- client: r.client,
|
|
|
|
- username: username,
|
|
|
|
- secret: secret,
|
|
|
|
|
|
+ refspec: refspec,
|
|
|
|
+ base: base,
|
|
|
|
+ client: r.client,
|
|
|
|
+ auth: r.auth,
|
|
}, nil
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
|
|
-func (r *dockerBase) getToken() string {
|
|
|
|
- r.mu.Lock()
|
|
|
|
- defer r.mu.Unlock()
|
|
|
|
-
|
|
|
|
- return r.token
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-func (r *dockerBase) setToken(token string) bool {
|
|
|
|
- r.mu.Lock()
|
|
|
|
- defer r.mu.Unlock()
|
|
|
|
-
|
|
|
|
- changed := r.token != token
|
|
|
|
- r.token = token
|
|
|
|
-
|
|
|
|
- return changed
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
func (r *dockerBase) url(ps ...string) string {
|
|
func (r *dockerBase) url(ps ...string) string {
|
|
url := r.base
|
|
url := r.base
|
|
url.Path = path.Join(url.Path, path.Join(ps...))
|
|
url.Path = path.Join(url.Path, path.Join(ps...))
|
|
return url.String()
|
|
return url.String()
|
|
}
|
|
}
|
|
|
|
|
|
-func (r *dockerBase) authorize(req *http.Request) {
|
|
|
|
- token := r.getToken()
|
|
|
|
- if r.useBasic {
|
|
|
|
- req.SetBasicAuth(r.username, r.secret)
|
|
|
|
- } else if token != "" {
|
|
|
|
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
|
|
|
|
+func (r *dockerBase) authorize(ctx context.Context, req *http.Request) error {
|
|
|
|
+ // Check if has header for host
|
|
|
|
+ if r.auth != nil {
|
|
|
|
+ if err := r.auth.Authorize(ctx, req); err != nil {
|
|
|
|
+ return err
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ return nil
|
|
}
|
|
}
|
|
|
|
|
|
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
|
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
|
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request")
|
|
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request")
|
|
- r.authorize(req)
|
|
|
|
|
|
+ if err := r.authorize(ctx, req); err != nil {
|
|
|
|
+ return nil, errors.Wrap(err, "failed to authorize")
|
|
|
|
+ }
|
|
resp, err := ctxhttp.Do(ctx, r.client, req)
|
|
resp, err := ctxhttp.Do(ctx, r.client, req)
|
|
if err != nil {
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to do request")
|
|
return nil, errors.Wrap(err, "failed to do request")
|
|
@@ -392,23 +389,14 @@ func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, respon
|
|
last := responses[len(responses)-1]
|
|
last := responses[len(responses)-1]
|
|
if last.StatusCode == http.StatusUnauthorized {
|
|
if last.StatusCode == http.StatusUnauthorized {
|
|
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
|
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
|
- for _, c := range parseAuthHeader(last.Header) {
|
|
|
|
- if c.scheme == bearerAuth {
|
|
|
|
- if err := invalidAuthorization(c, responses); err != nil {
|
|
|
|
- r.setToken("")
|
|
|
|
- return nil, err
|
|
|
|
- }
|
|
|
|
- if err := r.setTokenAuth(ctx, c.parameters); err != nil {
|
|
|
|
- return nil, err
|
|
|
|
- }
|
|
|
|
- return copyRequest(req)
|
|
|
|
- } else if c.scheme == basicAuth {
|
|
|
|
- if r.username != "" && r.secret != "" {
|
|
|
|
- r.useBasic = true
|
|
|
|
- }
|
|
|
|
|
|
+ if r.auth != nil {
|
|
|
|
+ if err := r.auth.AddResponses(ctx, responses); err == nil {
|
|
return copyRequest(req)
|
|
return copyRequest(req)
|
|
|
|
+ } else if !errdefs.IsNotImplemented(err) {
|
|
|
|
+ return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
return nil, nil
|
|
return nil, nil
|
|
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
|
|
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
|
|
// Support registries which have not properly implemented the HEAD method for
|
|
// Support registries which have not properly implemented the HEAD method for
|
|
@@ -424,30 +412,6 @@ func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, respon
|
|
return nil, nil
|
|
return nil, 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
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
func copyRequest(req *http.Request) (*http.Request, error) {
|
|
func copyRequest(req *http.Request) (*http.Request, error) {
|
|
ireq := *req
|
|
ireq := *req
|
|
if ireq.GetBody != nil {
|
|
if ireq.GetBody != nil {
|
|
@@ -459,167 +423,3 @@ func copyRequest(req *http.Request) (*http.Request, error) {
|
|
}
|
|
}
|
|
return &ireq, nil
|
|
return &ireq, nil
|
|
}
|
|
}
|
|
-
|
|
|
|
-func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) error {
|
|
|
|
- realm, ok := params["realm"]
|
|
|
|
- if !ok {
|
|
|
|
- return errors.New("no realm specified for token auth challenge")
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- realmURL, err := url.Parse(realm)
|
|
|
|
- if err != nil {
|
|
|
|
- return fmt.Errorf("invalid token auth challenge realm: %s", err)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- to := tokenOptions{
|
|
|
|
- realm: realmURL.String(),
|
|
|
|
- service: params["service"],
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- to.scopes = getTokenScopes(ctx, params)
|
|
|
|
- if len(to.scopes) == 0 {
|
|
|
|
- return errors.Errorf("no scope specified for token auth challenge")
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- var token string
|
|
|
|
- if r.secret != "" {
|
|
|
|
- // Credential information is provided, use oauth POST endpoint
|
|
|
|
- token, err = r.fetchTokenWithOAuth(ctx, to)
|
|
|
|
- if err != nil {
|
|
|
|
- return errors.Wrap(err, "failed to fetch oauth token")
|
|
|
|
- }
|
|
|
|
- } else {
|
|
|
|
- // Do request anonymously
|
|
|
|
- token, err = r.fetchToken(ctx, to)
|
|
|
|
- if err != nil {
|
|
|
|
- return errors.Wrap(err, "failed to fetch anonymous token")
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- r.setToken(token)
|
|
|
|
-
|
|
|
|
- return nil
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-type tokenOptions struct {
|
|
|
|
- realm string
|
|
|
|
- service string
|
|
|
|
- scopes []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 (r *dockerBase) 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-dist-tool")
|
|
|
|
-
|
|
|
|
- if r.username == "" {
|
|
|
|
- form.Set("grant_type", "refresh_token")
|
|
|
|
- form.Set("refresh_token", r.secret)
|
|
|
|
- } else {
|
|
|
|
- form.Set("grant_type", "password")
|
|
|
|
- form.Set("username", r.username)
|
|
|
|
- form.Set("password", r.secret)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form)
|
|
|
|
- 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 && r.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
|
|
|
|
- return r.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"`
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-// getToken fetches a token using a GET request
|
|
|
|
-func (r *dockerBase) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
|
|
|
|
- req, err := http.NewRequest("GET", to.realm, nil)
|
|
|
|
- if err != nil {
|
|
|
|
- return "", err
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- reqParams := req.URL.Query()
|
|
|
|
-
|
|
|
|
- if to.service != "" {
|
|
|
|
- reqParams.Add("service", to.service)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- for _, scope := range to.scopes {
|
|
|
|
- reqParams.Add("scope", scope)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if r.secret != "" {
|
|
|
|
- req.SetBasicAuth(r.username, r.secret)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- req.URL.RawQuery = reqParams.Encode()
|
|
|
|
-
|
|
|
|
- resp, err := ctxhttp.Do(ctx, r.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
|
|
|
|
-}
|
|
|