mirror of
https://github.com/rancher/os.git
synced 2025-08-31 22:32:14 +00:00
Bump libcompose and its dependencies
This commit is contained in:
11
vendor/github.com/docker/distribution/registry/client/auth/authchallenge.go
generated
vendored
11
vendor/github.com/docker/distribution/registry/client/auth/authchallenge.go
generated
vendored
@@ -25,7 +25,7 @@ type Challenge struct {
|
||||
type ChallengeManager interface {
|
||||
// GetChallenges returns the challenges for the given
|
||||
// endpoint URL.
|
||||
GetChallenges(endpoint string) ([]Challenge, error)
|
||||
GetChallenges(endpoint url.URL) ([]Challenge, error)
|
||||
|
||||
// AddResponse adds the response to the challenge
|
||||
// manager. The challenges will be parsed out of
|
||||
@@ -48,8 +48,10 @@ func NewSimpleChallengeManager() ChallengeManager {
|
||||
|
||||
type simpleChallengeManager map[string][]Challenge
|
||||
|
||||
func (m simpleChallengeManager) GetChallenges(endpoint string) ([]Challenge, error) {
|
||||
challenges := m[endpoint]
|
||||
func (m simpleChallengeManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
|
||||
endpoint.Host = strings.ToLower(endpoint.Host)
|
||||
|
||||
challenges := m[endpoint.String()]
|
||||
return challenges, nil
|
||||
}
|
||||
|
||||
@@ -60,11 +62,10 @@ func (m simpleChallengeManager) AddResponse(resp *http.Response) error {
|
||||
}
|
||||
urlCopy := url.URL{
|
||||
Path: resp.Request.URL.Path,
|
||||
Host: resp.Request.URL.Host,
|
||||
Host: strings.ToLower(resp.Request.URL.Host),
|
||||
Scheme: resp.Request.URL.Scheme,
|
||||
}
|
||||
m[urlCopy.String()] = challenges
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
38
vendor/github.com/docker/distribution/registry/client/auth/authchallenge_test.go
generated
vendored
38
vendor/github.com/docker/distribution/registry/client/auth/authchallenge_test.go
generated
vendored
@@ -1,38 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAuthChallengeParse(t *testing.T) {
|
||||
header := http.Header{}
|
||||
header.Add("WWW-Authenticate", `Bearer realm="https://auth.example.com/token",service="registry.example.com",other=fun,slashed="he\"\l\lo"`)
|
||||
|
||||
challenges := parseAuthHeader(header)
|
||||
if len(challenges) != 1 {
|
||||
t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges))
|
||||
}
|
||||
challenge := challenges[0]
|
||||
|
||||
if expected := "bearer"; challenge.Scheme != expected {
|
||||
t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected)
|
||||
}
|
||||
|
||||
if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected {
|
||||
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected)
|
||||
}
|
||||
|
||||
if expected := "registry.example.com"; challenge.Parameters["service"] != expected {
|
||||
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected)
|
||||
}
|
||||
|
||||
if expected := "fun"; challenge.Parameters["other"] != expected {
|
||||
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected)
|
||||
}
|
||||
|
||||
if expected := "he\"llo"; challenge.Parameters["slashed"] != expected {
|
||||
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected)
|
||||
}
|
||||
|
||||
}
|
308
vendor/github.com/docker/distribution/registry/client/auth/session.go
generated
vendored
308
vendor/github.com/docker/distribution/registry/client/auth/session.go
generated
vendored
@@ -15,6 +15,18 @@ import (
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoBasicAuthCredentials is returned if a request can't be authorized with
|
||||
// basic auth due to lack of credentials.
|
||||
ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
|
||||
|
||||
// ErrNoToken is returned if a request is successful but the body does not
|
||||
// contain an authorization token.
|
||||
ErrNoToken = errors.New("authorization server did not include a token in the response")
|
||||
)
|
||||
|
||||
const defaultClientID = "registry-client"
|
||||
|
||||
// AuthenticationHandler is an interface for authorizing a request from
|
||||
// params from a "WWW-Authenicate" header for a single scheme.
|
||||
type AuthenticationHandler interface {
|
||||
@@ -32,6 +44,14 @@ type AuthenticationHandler interface {
|
||||
type CredentialStore interface {
|
||||
// Basic returns basic auth for the given URL
|
||||
Basic(*url.URL) (string, string)
|
||||
|
||||
// RefreshToken returns a refresh token for the
|
||||
// given URL and service
|
||||
RefreshToken(*url.URL, string) string
|
||||
|
||||
// SetRefreshToken sets the refresh token if none
|
||||
// is provided for the given url and service
|
||||
SetRefreshToken(realm *url.URL, service, token string)
|
||||
}
|
||||
|
||||
// NewAuthorizer creates an authorizer which can handle multiple authentication
|
||||
@@ -63,9 +83,7 @@ func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
|
||||
Path: req.URL.Path[:v2Root+4],
|
||||
}
|
||||
|
||||
pingEndpoint := ping.String()
|
||||
|
||||
challenges, err := ea.challenges.GetChallenges(pingEndpoint)
|
||||
challenges, err := ea.challenges.GetChallenges(ping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,27 +119,47 @@ type clock interface {
|
||||
type tokenHandler struct {
|
||||
header http.Header
|
||||
creds CredentialStore
|
||||
scope tokenScope
|
||||
transport http.RoundTripper
|
||||
clock clock
|
||||
|
||||
offlineAccess bool
|
||||
forceOAuth bool
|
||||
clientID string
|
||||
scopes []Scope
|
||||
|
||||
tokenLock sync.Mutex
|
||||
tokenCache string
|
||||
tokenExpiration time.Time
|
||||
|
||||
additionalScopes map[string]struct{}
|
||||
}
|
||||
|
||||
// tokenScope represents the scope at which a token will be requested.
|
||||
// This represents a specific action on a registry resource.
|
||||
type tokenScope struct {
|
||||
Resource string
|
||||
Scope string
|
||||
Actions []string
|
||||
// Scope is a type which is serializable to a string
|
||||
// using the allow scope grammar.
|
||||
type Scope interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
func (ts tokenScope) String() string {
|
||||
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
|
||||
// RepositoryScope represents a token scope for access
|
||||
// to a repository.
|
||||
type RepositoryScope struct {
|
||||
Repository string
|
||||
Actions []string
|
||||
}
|
||||
|
||||
// String returns the string representation of the repository
|
||||
// using the scope grammar
|
||||
func (rs RepositoryScope) String() string {
|
||||
return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ","))
|
||||
}
|
||||
|
||||
// TokenHandlerOptions is used to configure a new token handler
|
||||
type TokenHandlerOptions struct {
|
||||
Transport http.RoundTripper
|
||||
Credentials CredentialStore
|
||||
|
||||
OfflineAccess bool
|
||||
ForceOAuth bool
|
||||
ClientID string
|
||||
Scopes []Scope
|
||||
}
|
||||
|
||||
// An implementation of clock for providing real time data.
|
||||
@@ -133,22 +171,33 @@ func (realClock) Now() time.Time { return time.Now() }
|
||||
// NewTokenHandler creates a new AuthenicationHandler which supports
|
||||
// fetching tokens from a remote token server.
|
||||
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
|
||||
return newTokenHandler(transport, creds, realClock{}, scope, actions...)
|
||||
// Create options...
|
||||
return NewTokenHandlerWithOptions(TokenHandlerOptions{
|
||||
Transport: transport,
|
||||
Credentials: creds,
|
||||
Scopes: []Scope{
|
||||
RepositoryScope{
|
||||
Repository: scope,
|
||||
Actions: actions,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// newTokenHandler exposes the option to provide a clock to manipulate time in unit testing.
|
||||
func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler {
|
||||
return &tokenHandler{
|
||||
transport: transport,
|
||||
creds: creds,
|
||||
clock: c,
|
||||
scope: tokenScope{
|
||||
Resource: "repository",
|
||||
Scope: scope,
|
||||
Actions: actions,
|
||||
},
|
||||
additionalScopes: map[string]struct{}{},
|
||||
// NewTokenHandlerWithOptions creates a new token handler using the provided
|
||||
// options structure.
|
||||
func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
|
||||
handler := &tokenHandler{
|
||||
transport: options.Transport,
|
||||
creds: options.Credentials,
|
||||
offlineAccess: options.OfflineAccess,
|
||||
forceOAuth: options.ForceOAuth,
|
||||
clientID: options.ClientID,
|
||||
scopes: options.Scopes,
|
||||
clock: realClock{},
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (th *tokenHandler) client() *http.Client {
|
||||
@@ -165,88 +214,162 @@ func (th *tokenHandler) Scheme() string {
|
||||
func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||
var additionalScopes []string
|
||||
if fromParam := req.URL.Query().Get("from"); fromParam != "" {
|
||||
additionalScopes = append(additionalScopes, tokenScope{
|
||||
Resource: "repository",
|
||||
Scope: fromParam,
|
||||
Actions: []string{"pull"},
|
||||
additionalScopes = append(additionalScopes, RepositoryScope{
|
||||
Repository: fromParam,
|
||||
Actions: []string{"pull"},
|
||||
}.String())
|
||||
}
|
||||
if err := th.refreshToken(params, additionalScopes...); err != nil {
|
||||
|
||||
token, err := th.getToken(params, additionalScopes...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error {
|
||||
func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) {
|
||||
th.tokenLock.Lock()
|
||||
defer th.tokenLock.Unlock()
|
||||
scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
|
||||
for _, scope := range th.scopes {
|
||||
scopes = append(scopes, scope.String())
|
||||
}
|
||||
var addedScopes bool
|
||||
for _, scope := range additionalScopes {
|
||||
if _, ok := th.additionalScopes[scope]; !ok {
|
||||
th.additionalScopes[scope] = struct{}{}
|
||||
addedScopes = true
|
||||
}
|
||||
scopes = append(scopes, scope)
|
||||
addedScopes = true
|
||||
}
|
||||
|
||||
now := th.clock.Now()
|
||||
if now.After(th.tokenExpiration) || addedScopes {
|
||||
tr, err := th.fetchToken(params)
|
||||
token, expiration, err := th.fetchToken(params, scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
th.tokenCache = tr.Token
|
||||
th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second)
|
||||
|
||||
// do not update cache for added scope tokens
|
||||
if !addedScopes {
|
||||
th.tokenCache = token
|
||||
th.tokenExpiration = expiration
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return th.tokenCache, nil
|
||||
}
|
||||
|
||||
type tokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
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 (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) {
|
||||
//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
|
||||
realm, ok := params["realm"]
|
||||
if !ok {
|
||||
return nil, errors.New("no realm specified for token auth challenge")
|
||||
func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
|
||||
form := url.Values{}
|
||||
form.Set("scope", strings.Join(scopes, " "))
|
||||
form.Set("service", service)
|
||||
|
||||
clientID := th.clientID
|
||||
if clientID == "" {
|
||||
// Use default client, this is a required field
|
||||
clientID = defaultClientID
|
||||
}
|
||||
form.Set("client_id", clientID)
|
||||
|
||||
if refreshToken != "" {
|
||||
form.Set("grant_type", "refresh_token")
|
||||
form.Set("refresh_token", refreshToken)
|
||||
} else if th.creds != nil {
|
||||
form.Set("grant_type", "password")
|
||||
username, password := th.creds.Basic(realm)
|
||||
form.Set("username", username)
|
||||
form.Set("password", password)
|
||||
|
||||
// attempt to get a refresh token
|
||||
form.Set("access_type", "offline")
|
||||
} else {
|
||||
// refuse to do oauth without a grant type
|
||||
return "", time.Time{}, fmt.Errorf("no supported grant type")
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): Handle empty scheme
|
||||
|
||||
realmURL, err := url.Parse(realm)
|
||||
resp, err := th.client().PostForm(realm.String(), form)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token auth challenge realm: %s", err)
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if !client.SuccessStatus(resp.StatusCode) {
|
||||
err := client.HandleErrorResponse(resp)
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", realmURL.String(), nil)
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
var tr postTokenResponse
|
||||
if err = decoder.Decode(&tr); err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
|
||||
}
|
||||
|
||||
if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
|
||||
th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
|
||||
}
|
||||
|
||||
if tr.ExpiresIn < minimumTokenLifetimeSeconds {
|
||||
// The default/minimum lifetime.
|
||||
tr.ExpiresIn = minimumTokenLifetimeSeconds
|
||||
logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
|
||||
}
|
||||
|
||||
if tr.IssuedAt.IsZero() {
|
||||
// issued_at is optional in the token response.
|
||||
tr.IssuedAt = th.clock.Now().UTC()
|
||||
}
|
||||
|
||||
return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), 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"`
|
||||
}
|
||||
|
||||
func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) {
|
||||
|
||||
req, err := http.NewRequest("GET", realm.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
reqParams := req.URL.Query()
|
||||
service := params["service"]
|
||||
scope := th.scope.String()
|
||||
|
||||
if service != "" {
|
||||
reqParams.Add("service", service)
|
||||
}
|
||||
|
||||
for _, scopeField := range strings.Fields(scope) {
|
||||
reqParams.Add("scope", scopeField)
|
||||
}
|
||||
|
||||
for scope := range th.additionalScopes {
|
||||
for _, scope := range scopes {
|
||||
reqParams.Add("scope", scope)
|
||||
}
|
||||
|
||||
if th.offlineAccess {
|
||||
reqParams.Add("offline_token", "true")
|
||||
clientID := th.clientID
|
||||
if clientID == "" {
|
||||
clientID = defaultClientID
|
||||
}
|
||||
reqParams.Add("client_id", clientID)
|
||||
}
|
||||
|
||||
if th.creds != nil {
|
||||
username, password := th.creds.Basic(realmURL)
|
||||
username, password := th.creds.Basic(realm)
|
||||
if username != "" && password != "" {
|
||||
reqParams.Add("account", username)
|
||||
req.SetBasicAuth(username, password)
|
||||
@@ -257,20 +380,24 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
|
||||
|
||||
resp, err := th.client().Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if !client.SuccessStatus(resp.StatusCode) {
|
||||
err := client.HandleErrorResponse(resp)
|
||||
return nil, err
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
tr := new(tokenResponse)
|
||||
if err = decoder.Decode(tr); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode token response: %s", err)
|
||||
var tr getTokenResponse
|
||||
if err = decoder.Decode(&tr); err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
|
||||
}
|
||||
|
||||
if tr.RefreshToken != "" && th.creds != nil {
|
||||
th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
|
||||
}
|
||||
|
||||
// `access_token` is equivalent to `token` and if both are specified
|
||||
@@ -281,21 +408,48 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
|
||||
}
|
||||
|
||||
if tr.Token == "" {
|
||||
return nil, errors.New("authorization server did not include a token in the response")
|
||||
return "", time.Time{}, ErrNoToken
|
||||
}
|
||||
|
||||
if tr.ExpiresIn < minimumTokenLifetimeSeconds {
|
||||
logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
|
||||
// The default/minimum lifetime.
|
||||
tr.ExpiresIn = minimumTokenLifetimeSeconds
|
||||
logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
|
||||
}
|
||||
|
||||
if tr.IssuedAt.IsZero() {
|
||||
// issued_at is optional in the token response.
|
||||
tr.IssuedAt = th.clock.Now()
|
||||
tr.IssuedAt = th.clock.Now().UTC()
|
||||
}
|
||||
|
||||
return tr, nil
|
||||
return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
|
||||
}
|
||||
|
||||
func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
|
||||
realm, ok := params["realm"]
|
||||
if !ok {
|
||||
return "", time.Time{}, errors.New("no realm specified for token auth challenge")
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): Handle empty scheme and relative realm
|
||||
realmURL, err := url.Parse(realm)
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err)
|
||||
}
|
||||
|
||||
service := params["service"]
|
||||
|
||||
var refreshToken string
|
||||
|
||||
if th.creds != nil {
|
||||
refreshToken = th.creds.RefreshToken(realmURL, service)
|
||||
}
|
||||
|
||||
if refreshToken != "" || th.forceOAuth {
|
||||
return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
|
||||
}
|
||||
|
||||
return th.fetchTokenWithBasicAuth(realmURL, service, scopes)
|
||||
}
|
||||
|
||||
type basicHandler struct {
|
||||
@@ -322,5 +476,5 @@ func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]st
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("no basic auth credentials")
|
||||
return ErrNoBasicAuthCredentials
|
||||
}
|
||||
|
599
vendor/github.com/docker/distribution/registry/client/auth/session_test.go
generated
vendored
599
vendor/github.com/docker/distribution/registry/client/auth/session_test.go
generated
vendored
@@ -1,599 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/distribution/testutil"
|
||||
)
|
||||
|
||||
// An implementation of clock for providing fake time data.
|
||||
type fakeClock struct {
|
||||
current time.Time
|
||||
}
|
||||
|
||||
// Now implements clock
|
||||
func (fc *fakeClock) Now() time.Time { return fc.current }
|
||||
|
||||
func testServer(rrm testutil.RequestResponseMap) (string, func()) {
|
||||
h := testutil.NewHandler(rrm)
|
||||
s := httptest.NewServer(h)
|
||||
return s.URL, s.Close
|
||||
}
|
||||
|
||||
type testAuthenticationWrapper struct {
|
||||
headers http.Header
|
||||
authCheck func(string) bool
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func (w *testAuthenticationWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" || !w.authCheck(auth) {
|
||||
h := rw.Header()
|
||||
for k, values := range w.headers {
|
||||
h[k] = values
|
||||
}
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.next.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func testServerWithAuth(rrm testutil.RequestResponseMap, authenticate string, authCheck func(string) bool) (string, func()) {
|
||||
h := testutil.NewHandler(rrm)
|
||||
wrapper := &testAuthenticationWrapper{
|
||||
|
||||
headers: http.Header(map[string][]string{
|
||||
"X-API-Version": {"registry/2.0"},
|
||||
"X-Multi-API-Version": {"registry/2.0", "registry/2.1", "trust/1.0"},
|
||||
"WWW-Authenticate": {authenticate},
|
||||
}),
|
||||
authCheck: authCheck,
|
||||
next: h,
|
||||
}
|
||||
|
||||
s := httptest.NewServer(wrapper)
|
||||
return s.URL, s.Close
|
||||
}
|
||||
|
||||
// ping pings the provided endpoint to determine its required authorization challenges.
|
||||
// If a version header is provided, the versions will be returned.
|
||||
func ping(manager ChallengeManager, endpoint, versionHeader string) ([]APIVersion, error) {
|
||||
resp, err := http.Get(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := manager.AddResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return APIVersions(resp, versionHeader), err
|
||||
}
|
||||
|
||||
type testCredentialStore struct {
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (tcs *testCredentialStore) Basic(*url.URL) (string, string) {
|
||||
return tcs.username, tcs.password
|
||||
}
|
||||
|
||||
func TestEndpointAuthorizeToken(t *testing.T) {
|
||||
service := "localhost.localdomain"
|
||||
repo1 := "some/registry"
|
||||
repo2 := "other/registry"
|
||||
scope1 := fmt.Sprintf("repository:%s:pull,push", repo1)
|
||||
scope2 := fmt.Sprintf("repository:%s:pull,push", repo2)
|
||||
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope1), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"token":"statictoken"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope2), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"token":"badtoken"}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
te, tc := testServer(tokenMap)
|
||||
defer tc()
|
||||
|
||||
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
||||
validCheck := func(a string) bool {
|
||||
return a == "Bearer statictoken"
|
||||
}
|
||||
e, c := testServerWithAuth(m, authenicate, validCheck)
|
||||
defer c()
|
||||
|
||||
challengeManager1 := NewSimpleChallengeManager()
|
||||
versions, err := ping(challengeManager1, e+"/v2/", "x-api-version")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(versions) != 1 {
|
||||
t.Fatalf("Unexpected version count: %d, expected 1", len(versions))
|
||||
}
|
||||
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
||||
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
||||
}
|
||||
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, nil, repo1, "pull", "push")))
|
||||
client := &http.Client{Transport: transport1}
|
||||
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
|
||||
badCheck := func(a string) bool {
|
||||
return a == "Bearer statictoken"
|
||||
}
|
||||
e2, c2 := testServerWithAuth(m, authenicate, badCheck)
|
||||
defer c2()
|
||||
|
||||
challengeManager2 := NewSimpleChallengeManager()
|
||||
versions, err = ping(challengeManager2, e+"/v2/", "x-multi-api-version")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(versions) != 3 {
|
||||
t.Fatalf("Unexpected version count: %d, expected 3", len(versions))
|
||||
}
|
||||
if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check {
|
||||
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check)
|
||||
}
|
||||
if check := (APIVersion{Type: "registry", Version: "2.1"}); versions[1] != check {
|
||||
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[1], check)
|
||||
}
|
||||
if check := (APIVersion{Type: "trust", Version: "1.0"}); versions[2] != check {
|
||||
t.Fatalf("Unexpected api version: %#v, expected %#v", versions[2], check)
|
||||
}
|
||||
transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, nil, repo2, "pull", "push")))
|
||||
client2 := &http.Client{Transport: transport2}
|
||||
|
||||
req, _ = http.NewRequest("GET", e2+"/v2/hello", nil)
|
||||
resp, err = client2.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func basicAuth(username, password string) string {
|
||||
auth := username + ":" + password
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
func TestEndpointAuthorizeTokenBasic(t *testing.T) {
|
||||
service := "localhost.localdomain"
|
||||
repo := "some/fun/registry"
|
||||
scope := fmt.Sprintf("repository:%s:pull,push", repo)
|
||||
username := "tokenuser"
|
||||
password := "superSecretPa$$word"
|
||||
|
||||
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"access_token":"statictoken"}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate1 := fmt.Sprintf("Basic realm=localhost")
|
||||
basicCheck := func(a string) bool {
|
||||
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
||||
}
|
||||
te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
|
||||
defer tc()
|
||||
|
||||
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
||||
bearerCheck := func(a string) bool {
|
||||
return a == "Bearer statictoken"
|
||||
}
|
||||
e, c := testServerWithAuth(m, authenicate2, bearerCheck)
|
||||
defer c()
|
||||
|
||||
creds := &testCredentialStore{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
challengeManager := NewSimpleChallengeManager()
|
||||
_, err := ping(challengeManager, e+"/v2/", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewTokenHandler(nil, creds, repo, "pull", "push"), NewBasicHandler(creds)))
|
||||
client := &http.Client{Transport: transport1}
|
||||
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) {
|
||||
service := "localhost.localdomain"
|
||||
repo := "some/fun/registry"
|
||||
scope := fmt.Sprintf("repository:%s:pull,push", repo)
|
||||
username := "tokenuser"
|
||||
password := "superSecretPa$$word"
|
||||
|
||||
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"token":"statictoken", "expires_in": 3001}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"access_token":"statictoken", "expires_in": 3001}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate1 := fmt.Sprintf("Basic realm=localhost")
|
||||
tokenExchanges := 0
|
||||
basicCheck := func(a string) bool {
|
||||
tokenExchanges = tokenExchanges + 1
|
||||
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
||||
}
|
||||
te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
|
||||
defer tc()
|
||||
|
||||
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
||||
bearerCheck := func(a string) bool {
|
||||
return a == "Bearer statictoken"
|
||||
}
|
||||
e, c := testServerWithAuth(m, authenicate2, bearerCheck)
|
||||
defer c()
|
||||
|
||||
creds := &testCredentialStore{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
challengeManager := NewSimpleChallengeManager()
|
||||
_, err := ping(challengeManager, e+"/v2/", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clock := &fakeClock{current: time.Now()}
|
||||
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds)))
|
||||
client := &http.Client{Transport: transport1}
|
||||
|
||||
// First call should result in a token exchange
|
||||
// Subsequent calls should recycle the token from the first request, until the expiration has lapsed.
|
||||
timeIncrement := 1000 * time.Second
|
||||
for i := 0; i < 4; i++ {
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
if tokenExchanges != 1 {
|
||||
t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i)
|
||||
}
|
||||
clock.current = clock.current.Add(timeIncrement)
|
||||
}
|
||||
|
||||
// After we've exceeded the expiration, we should see a second token exchange.
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
if tokenExchanges != 2 {
|
||||
t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) {
|
||||
service := "localhost.localdomain"
|
||||
repo := "some/fun/registry"
|
||||
scope := fmt.Sprintf("repository:%s:pull,push", repo)
|
||||
username := "tokenuser"
|
||||
password := "superSecretPa$$word"
|
||||
|
||||
// This test sets things up such that the token was issued one increment
|
||||
// earlier than its sibling in TestEndpointAuthorizeTokenBasicWithExpiresIn.
|
||||
// This will mean that the token expires after 3 increments instead of 4.
|
||||
clock := &fakeClock{current: time.Now()}
|
||||
timeIncrement := 1000 * time.Second
|
||||
firstIssuedAt := clock.Now()
|
||||
clock.current = clock.current.Add(timeIncrement)
|
||||
secondIssuedAt := clock.current.Add(2 * timeIncrement)
|
||||
tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"token":"statictoken", "issued_at": "` + firstIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service),
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: []byte(`{"access_token":"statictoken", "issued_at": "` + secondIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate1 := fmt.Sprintf("Basic realm=localhost")
|
||||
tokenExchanges := 0
|
||||
basicCheck := func(a string) bool {
|
||||
tokenExchanges = tokenExchanges + 1
|
||||
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
||||
}
|
||||
te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck)
|
||||
defer tc()
|
||||
|
||||
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service)
|
||||
bearerCheck := func(a string) bool {
|
||||
return a == "Bearer statictoken"
|
||||
}
|
||||
e, c := testServerWithAuth(m, authenicate2, bearerCheck)
|
||||
defer c()
|
||||
|
||||
creds := &testCredentialStore{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
challengeManager := NewSimpleChallengeManager()
|
||||
_, err := ping(challengeManager, e+"/v2/", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds)))
|
||||
client := &http.Client{Transport: transport1}
|
||||
|
||||
// First call should result in a token exchange
|
||||
// Subsequent calls should recycle the token from the first request, until the expiration has lapsed.
|
||||
// We shaved one increment off of the equivalent logic in TestEndpointAuthorizeTokenBasicWithExpiresIn
|
||||
// so this loop should have one fewer iteration.
|
||||
for i := 0; i < 3; i++ {
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
if tokenExchanges != 1 {
|
||||
t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i)
|
||||
}
|
||||
clock.current = clock.current.Add(timeIncrement)
|
||||
}
|
||||
|
||||
// After we've exceeded the expiration, we should see a second token exchange.
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
if tokenExchanges != 2 {
|
||||
t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpointAuthorizeBasic(t *testing.T) {
|
||||
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/hello",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
username := "user1"
|
||||
password := "funSecretPa$$word"
|
||||
authenicate := fmt.Sprintf("Basic realm=localhost")
|
||||
validCheck := func(a string) bool {
|
||||
return a == fmt.Sprintf("Basic %s", basicAuth(username, password))
|
||||
}
|
||||
e, c := testServerWithAuth(m, authenicate, validCheck)
|
||||
defer c()
|
||||
creds := &testCredentialStore{
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
challengeManager := NewSimpleChallengeManager()
|
||||
_, err := ping(challengeManager, e+"/v2/", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewBasicHandler(creds)))
|
||||
client := &http.Client{Transport: transport1}
|
||||
|
||||
req, _ := http.NewRequest("GET", e+"/v2/hello", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending get request: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted)
|
||||
}
|
||||
}
|
18
vendor/github.com/docker/distribution/registry/client/blob_writer.go
generated
vendored
18
vendor/github.com/docker/distribution/registry/client/blob_writer.go
generated
vendored
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
@@ -104,21 +103,8 @@ func (hbu *httpBlobUpload) Write(p []byte) (n int, err error) {
|
||||
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) Seek(offset int64, whence int) (int64, error) {
|
||||
newOffset := hbu.offset
|
||||
|
||||
switch whence {
|
||||
case os.SEEK_CUR:
|
||||
newOffset += int64(offset)
|
||||
case os.SEEK_END:
|
||||
newOffset += int64(offset)
|
||||
case os.SEEK_SET:
|
||||
newOffset = int64(offset)
|
||||
}
|
||||
|
||||
hbu.offset = newOffset
|
||||
|
||||
return hbu.offset, nil
|
||||
func (hbu *httpBlobUpload) Size() int64 {
|
||||
return hbu.offset
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) ID() string {
|
||||
|
211
vendor/github.com/docker/distribution/registry/client/blob_writer_test.go
generated
vendored
211
vendor/github.com/docker/distribution/registry/client/blob_writer_test.go
generated
vendored
@@ -1,211 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/testutil"
|
||||
)
|
||||
|
||||
// Test implements distribution.BlobWriter
|
||||
var _ distribution.BlobWriter = &httpBlobUpload{}
|
||||
|
||||
func TestUploadReadFrom(t *testing.T) {
|
||||
_, b := newRandomBlob(64)
|
||||
repo := "test/upload/readfrom"
|
||||
locationPath := fmt.Sprintf("/v2/%s/uploads/testid", repo)
|
||||
|
||||
m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/",
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Headers: http.Header(map[string][]string{
|
||||
"Docker-Distribution-API-Version": {"registry/2.0"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Test Valid case
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "PATCH",
|
||||
Route: locationPath,
|
||||
Body: b,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Headers: http.Header(map[string][]string{
|
||||
"Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"},
|
||||
"Location": {locationPath},
|
||||
"Range": {"0-63"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Test invalid range
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "PATCH",
|
||||
Route: locationPath,
|
||||
Body: b,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Headers: http.Header(map[string][]string{
|
||||
"Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"},
|
||||
"Location": {locationPath},
|
||||
"Range": {""},
|
||||
}),
|
||||
},
|
||||
},
|
||||
// Test 404
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "PATCH",
|
||||
Route: locationPath,
|
||||
Body: b,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
// Test 400 valid json
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "PATCH",
|
||||
Route: locationPath,
|
||||
Body: b,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: []byte(`
|
||||
{ "errors":
|
||||
[
|
||||
{
|
||||
"code": "BLOB_UPLOAD_INVALID",
|
||||
"message": "blob upload invalid",
|
||||
"detail": "more detail"
|
||||
}
|
||||
]
|
||||
} `),
|
||||
},
|
||||
},
|
||||
// Test 400 invalid json
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "PATCH",
|
||||
Route: locationPath,
|
||||
Body: b,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: []byte("something bad happened"),
|
||||
},
|
||||
},
|
||||
// Test 500
|
||||
{
|
||||
Request: testutil.Request{
|
||||
Method: "PATCH",
|
||||
Route: locationPath,
|
||||
Body: b,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
e, c := testServer(m)
|
||||
defer c()
|
||||
|
||||
blobUpload := &httpBlobUpload{
|
||||
client: &http.Client{},
|
||||
}
|
||||
|
||||
// Valid case
|
||||
blobUpload.location = e + locationPath
|
||||
n, err := blobUpload.ReadFrom(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling ReadFrom: %s", err)
|
||||
}
|
||||
if n != 64 {
|
||||
t.Fatalf("Wrong length returned from ReadFrom: %d, expected 64", n)
|
||||
}
|
||||
|
||||
// Bad range
|
||||
blobUpload.location = e + locationPath
|
||||
_, err = blobUpload.ReadFrom(bytes.NewReader(b))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error when bad range received")
|
||||
}
|
||||
|
||||
// 404
|
||||
blobUpload.location = e + locationPath
|
||||
_, err = blobUpload.ReadFrom(bytes.NewReader(b))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error when not found")
|
||||
}
|
||||
if err != distribution.ErrBlobUploadUnknown {
|
||||
t.Fatalf("Wrong error thrown: %s, expected %s", err, distribution.ErrBlobUploadUnknown)
|
||||
}
|
||||
|
||||
// 400 valid json
|
||||
blobUpload.location = e + locationPath
|
||||
_, err = blobUpload.ReadFrom(bytes.NewReader(b))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error when not found")
|
||||
}
|
||||
if uploadErr, ok := err.(errcode.Errors); !ok {
|
||||
t.Fatalf("Wrong error type %T: %s", err, err)
|
||||
} else if len(uploadErr) != 1 {
|
||||
t.Fatalf("Unexpected number of errors: %d, expected 1", len(uploadErr))
|
||||
} else {
|
||||
v2Err, ok := uploadErr[0].(errcode.Error)
|
||||
if !ok {
|
||||
t.Fatalf("Not an 'Error' type: %#v", uploadErr[0])
|
||||
}
|
||||
if v2Err.Code != v2.ErrorCodeBlobUploadInvalid {
|
||||
t.Fatalf("Unexpected error code: %s, expected %d", v2Err.Code.String(), v2.ErrorCodeBlobUploadInvalid)
|
||||
}
|
||||
if expected := "blob upload invalid"; v2Err.Message != expected {
|
||||
t.Fatalf("Unexpected error message: %q, expected %q", v2Err.Message, expected)
|
||||
}
|
||||
if expected := "more detail"; v2Err.Detail.(string) != expected {
|
||||
t.Fatalf("Unexpected error message: %q, expected %q", v2Err.Detail.(string), expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 400 invalid json
|
||||
blobUpload.location = e + locationPath
|
||||
_, err = blobUpload.ReadFrom(bytes.NewReader(b))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error when not found")
|
||||
}
|
||||
if uploadErr, ok := err.(*UnexpectedHTTPResponseError); !ok {
|
||||
t.Fatalf("Wrong error type %T: %s", err, err)
|
||||
} else {
|
||||
respStr := string(uploadErr.Response)
|
||||
if expected := "something bad happened"; respStr != expected {
|
||||
t.Fatalf("Unexpected response string: %s, expected: %s", respStr, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 500
|
||||
blobUpload.location = e + locationPath
|
||||
_, err = blobUpload.ReadFrom(bytes.NewReader(b))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error when not found")
|
||||
}
|
||||
if uploadErr, ok := err.(*UnexpectedHTTPStatusError); !ok {
|
||||
t.Fatalf("Wrong error type %T: %s", err, err)
|
||||
} else if expected := "500 " + http.StatusText(http.StatusInternalServerError); uploadErr.Status != expected {
|
||||
t.Fatalf("Unexpected response status: %s, expected %s", uploadErr.Status, expected)
|
||||
}
|
||||
}
|
30
vendor/github.com/docker/distribution/registry/client/errors.go
generated
vendored
30
vendor/github.com/docker/distribution/registry/client/errors.go
generated
vendored
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -10,6 +11,10 @@ import (
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
)
|
||||
|
||||
// ErrNoErrorsInBody is returned when a HTTP response body parses to an empty
|
||||
// errcode.Errors slice.
|
||||
var ErrNoErrorsInBody = errors.New("no error details found in HTTP response body")
|
||||
|
||||
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
|
||||
// returned when making a registry api call.
|
||||
type UnexpectedHTTPStatusError struct {
|
||||
@@ -17,18 +22,19 @@ type UnexpectedHTTPStatusError struct {
|
||||
}
|
||||
|
||||
func (e *UnexpectedHTTPStatusError) Error() string {
|
||||
return fmt.Sprintf("Received unexpected HTTP status: %s", e.Status)
|
||||
return fmt.Sprintf("received unexpected HTTP status: %s", e.Status)
|
||||
}
|
||||
|
||||
// UnexpectedHTTPResponseError is returned when an expected HTTP status code
|
||||
// is returned, but the content was unexpected and failed to be parsed.
|
||||
type UnexpectedHTTPResponseError struct {
|
||||
ParseErr error
|
||||
Response []byte
|
||||
ParseErr error
|
||||
StatusCode int
|
||||
Response []byte
|
||||
}
|
||||
|
||||
func (e *UnexpectedHTTPResponseError) Error() string {
|
||||
return fmt.Sprintf("Error parsing HTTP response: %s: %q", e.ParseErr.Error(), string(e.Response))
|
||||
return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response))
|
||||
}
|
||||
|
||||
func parseHTTPErrorResponse(statusCode int, r io.Reader) error {
|
||||
@@ -53,10 +59,22 @@ func parseHTTPErrorResponse(statusCode int, r io.Reader) error {
|
||||
|
||||
if err := json.Unmarshal(body, &errors); err != nil {
|
||||
return &UnexpectedHTTPResponseError{
|
||||
ParseErr: err,
|
||||
Response: body,
|
||||
ParseErr: err,
|
||||
StatusCode: statusCode,
|
||||
Response: body,
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
// If there was no error specified in the body, return
|
||||
// UnexpectedHTTPResponseError.
|
||||
return &UnexpectedHTTPResponseError{
|
||||
ParseErr: ErrNoErrorsInBody,
|
||||
StatusCode: statusCode,
|
||||
Response: body,
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
|
89
vendor/github.com/docker/distribution/registry/client/errors_test.go
generated
vendored
89
vendor/github.com/docker/distribution/registry/client/errors_test.go
generated
vendored
@@ -1,89 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type nopCloser struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
func TestHandleErrorResponse401ValidBody(t *testing.T) {
|
||||
json := "{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"action requires authentication\"}]}"
|
||||
response := &http.Response{
|
||||
Status: "401 Unauthorized",
|
||||
StatusCode: 401,
|
||||
Body: nopCloser{bytes.NewBufferString(json)},
|
||||
}
|
||||
err := HandleErrorResponse(response)
|
||||
|
||||
expectedMsg := "unauthorized: action requires authentication"
|
||||
if !strings.Contains(err.Error(), expectedMsg) {
|
||||
t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleErrorResponse401WithInvalidBody(t *testing.T) {
|
||||
json := "{invalid json}"
|
||||
response := &http.Response{
|
||||
Status: "401 Unauthorized",
|
||||
StatusCode: 401,
|
||||
Body: nopCloser{bytes.NewBufferString(json)},
|
||||
}
|
||||
err := HandleErrorResponse(response)
|
||||
|
||||
expectedMsg := "unauthorized: authentication required"
|
||||
if !strings.Contains(err.Error(), expectedMsg) {
|
||||
t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleErrorResponseExpectedStatusCode400ValidBody(t *testing.T) {
|
||||
json := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest does not match\"}]}"
|
||||
response := &http.Response{
|
||||
Status: "400 Bad Request",
|
||||
StatusCode: 400,
|
||||
Body: nopCloser{bytes.NewBufferString(json)},
|
||||
}
|
||||
err := HandleErrorResponse(response)
|
||||
|
||||
expectedMsg := "digest invalid: provided digest does not match"
|
||||
if !strings.Contains(err.Error(), expectedMsg) {
|
||||
t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleErrorResponseExpectedStatusCode404InvalidBody(t *testing.T) {
|
||||
json := "{invalid json}"
|
||||
response := &http.Response{
|
||||
Status: "404 Not Found",
|
||||
StatusCode: 404,
|
||||
Body: nopCloser{bytes.NewBufferString(json)},
|
||||
}
|
||||
err := HandleErrorResponse(response)
|
||||
|
||||
expectedMsg := "Error parsing HTTP response: invalid character 'i' looking for beginning of object key string: \"{invalid json}\""
|
||||
if !strings.Contains(err.Error(), expectedMsg) {
|
||||
t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleErrorResponseUnexpectedStatusCode501(t *testing.T) {
|
||||
response := &http.Response{
|
||||
Status: "501 Not Implemented",
|
||||
StatusCode: 501,
|
||||
Body: nopCloser{bytes.NewBufferString("{\"Error Encountered\" : \"Function not implemented.\"}")},
|
||||
}
|
||||
err := HandleErrorResponse(response)
|
||||
|
||||
expectedMsg := "Received unexpected HTTP status: 501 Not Implemented"
|
||||
if !strings.Contains(err.Error(), expectedMsg) {
|
||||
t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error())
|
||||
}
|
||||
}
|
120
vendor/github.com/docker/distribution/registry/client/repository.go
generated
vendored
120
vendor/github.com/docker/distribution/registry/client/repository.go
generated
vendored
@@ -27,16 +27,50 @@ type Registry interface {
|
||||
Repositories(ctx context.Context, repos []string, last string) (n int, err error)
|
||||
}
|
||||
|
||||
// checkHTTPRedirect is a callback that can manipulate redirected HTTP
|
||||
// requests. It is used to preserve Accept and Range headers.
|
||||
func checkHTTPRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return errors.New("stopped after 10 redirects")
|
||||
}
|
||||
|
||||
if len(via) > 0 {
|
||||
for headerName, headerVals := range via[0].Header {
|
||||
if headerName != "Accept" && headerName != "Range" {
|
||||
continue
|
||||
}
|
||||
for _, val := range headerVals {
|
||||
// Don't add to redirected request if redirected
|
||||
// request already has a header with the same
|
||||
// name and value.
|
||||
hasValue := false
|
||||
for _, existingVal := range req.Header[headerName] {
|
||||
if existingVal == val {
|
||||
hasValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasValue {
|
||||
req.Header.Add(headerName, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRegistry creates a registry namespace which can be used to get a listing of repositories
|
||||
func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) {
|
||||
ub, err := v2.NewURLBuilderFromString(baseURL)
|
||||
ub, err := v2.NewURLBuilderFromString(baseURL, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 1 * time.Minute,
|
||||
Transport: transport,
|
||||
Timeout: 1 * time.Minute,
|
||||
CheckRedirect: checkHTTPRedirect,
|
||||
}
|
||||
|
||||
return ®istry{
|
||||
@@ -99,13 +133,14 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri
|
||||
|
||||
// NewRepository creates a new Repository for the given repository name and base URL.
|
||||
func NewRepository(ctx context.Context, name reference.Named, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
|
||||
ub, err := v2.NewURLBuilderFromString(baseURL)
|
||||
ub, err := v2.NewURLBuilderFromString(baseURL, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Transport: transport,
|
||||
CheckRedirect: checkHTTPRedirect,
|
||||
// TODO(dmcgowan): create cookie jar
|
||||
}
|
||||
|
||||
@@ -124,7 +159,7 @@ type repository struct {
|
||||
name reference.Named
|
||||
}
|
||||
|
||||
func (r *repository) Name() reference.Named {
|
||||
func (r *repository) Named() reference.Named {
|
||||
return r.name
|
||||
}
|
||||
|
||||
@@ -157,7 +192,7 @@ func (r *repository) Tags(ctx context.Context) distribution.TagService {
|
||||
client: r.client,
|
||||
ub: r.ub,
|
||||
context: r.context,
|
||||
name: r.Name(),
|
||||
name: r.Named(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,19 +292,38 @@ func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, er
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
var attempts int
|
||||
resp, err := t.client.Head(u)
|
||||
|
||||
req, err := http.NewRequest("HEAD", u, nil)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
for _, t := range distribution.ManifestMediaTypes() {
|
||||
req.Header.Add("Accept", t)
|
||||
}
|
||||
|
||||
var attempts int
|
||||
resp, err := t.client.Do(req)
|
||||
check:
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch {
|
||||
case resp.StatusCode >= 200 && resp.StatusCode < 400:
|
||||
return descriptorFromResponse(resp)
|
||||
case resp.StatusCode == http.StatusMethodNotAllowed:
|
||||
resp, err = t.client.Get(u)
|
||||
req, err = http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
for _, t := range distribution.ManifestMediaTypes() {
|
||||
req.Header.Add("Accept", t)
|
||||
}
|
||||
|
||||
resp, err = t.client.Do(req)
|
||||
attempts++
|
||||
if attempts > 1 {
|
||||
return distribution.Descriptor{}, err
|
||||
@@ -348,9 +402,9 @@ func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...dis
|
||||
)
|
||||
|
||||
for _, option := range options {
|
||||
if opt, ok := option.(withTagOption); ok {
|
||||
digestOrTag = opt.tag
|
||||
ref, err = reference.WithTag(ms.name, opt.tag)
|
||||
if opt, ok := option.(distribution.WithTagOption); ok {
|
||||
digestOrTag = opt.Tag
|
||||
ref, err = reference.WithTag(ms.name, opt.Tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -411,33 +465,20 @@ func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...dis
|
||||
return nil, HandleErrorResponse(resp)
|
||||
}
|
||||
|
||||
// WithTag allows a tag to be passed into Put which enables the client
|
||||
// to build a correct URL.
|
||||
func WithTag(tag string) distribution.ManifestServiceOption {
|
||||
return withTagOption{tag}
|
||||
}
|
||||
|
||||
type withTagOption struct{ tag string }
|
||||
|
||||
func (o withTagOption) Apply(m distribution.ManifestService) error {
|
||||
if _, ok := m.(*manifests); ok {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("withTagOption is a client-only option")
|
||||
}
|
||||
|
||||
// Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the
|
||||
// tag name in order to build the correct upload URL. This state is written and read under a lock.
|
||||
// tag name in order to build the correct upload URL.
|
||||
func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
|
||||
ref := ms.name
|
||||
var tagged bool
|
||||
|
||||
for _, option := range options {
|
||||
if opt, ok := option.(withTagOption); ok {
|
||||
if opt, ok := option.(distribution.WithTagOption); ok {
|
||||
var err error
|
||||
ref, err = reference.WithTag(ref, opt.tag)
|
||||
ref, err = reference.WithTag(ref, opt.Tag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tagged = true
|
||||
} else {
|
||||
err := option.Apply(ms)
|
||||
if err != nil {
|
||||
@@ -445,13 +486,24 @@ func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options .
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifestURL, err := ms.ub.BuildManifestURL(ref)
|
||||
mediaType, p, err := m.Payload()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mediaType, p, err := m.Payload()
|
||||
if !tagged {
|
||||
// generate a canonical digest and Put by digest
|
||||
_, d, err := distribution.UnmarshalManifest(mediaType, p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ref, err = reference.WithDigest(ref, d.Digest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
manifestURL, err := ms.ub.BuildManifestURL(ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
1073
vendor/github.com/docker/distribution/registry/client/repository_test.go
generated
vendored
1073
vendor/github.com/docker/distribution/registry/client/repository_test.go
generated
vendored
File diff suppressed because it is too large
Load Diff
99
vendor/github.com/docker/distribution/registry/client/transport/http_reader.go
generated
vendored
99
vendor/github.com/docker/distribution/registry/client/transport/http_reader.go
generated
vendored
@@ -1,12 +1,22 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`)
|
||||
|
||||
// ErrWrongCodeForByteRange is returned if the client sends a request
|
||||
// with a Range header but the server returns a 2xx or 3xx code other
|
||||
// than 206 Partial Content.
|
||||
ErrWrongCodeForByteRange = errors.New("expected HTTP 206 from byte range request")
|
||||
)
|
||||
|
||||
// ReadSeekCloser combines io.ReadSeeker with io.Closer.
|
||||
@@ -40,8 +50,6 @@ type httpReadSeeker struct {
|
||||
|
||||
// rc is the remote read closer.
|
||||
rc io.ReadCloser
|
||||
// brd is a buffer for internal buffered io.
|
||||
brd *bufio.Reader
|
||||
// readerOffset tracks the offset as of the last read.
|
||||
readerOffset int64
|
||||
// seekOffset allows Seek to override the offset. Seek changes
|
||||
@@ -58,7 +66,7 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
|
||||
return 0, hrs.err
|
||||
}
|
||||
|
||||
// If we seeked to a different position, we need to reset the
|
||||
// If we sought to a different position, we need to reset the
|
||||
// connection. This logic is here instead of Seek so that if
|
||||
// a seek is undone before the next read, the connection doesn't
|
||||
// need to be closed and reopened. A common example of this is
|
||||
@@ -79,11 +87,6 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
|
||||
hrs.seekOffset += int64(n)
|
||||
hrs.readerOffset += int64(n)
|
||||
|
||||
// Simulate io.EOF error if we reach filesize.
|
||||
if err == nil && hrs.size >= 0 && hrs.readerOffset >= hrs.size {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -92,8 +95,18 @@ func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, hrs.err
|
||||
}
|
||||
|
||||
lastReaderOffset := hrs.readerOffset
|
||||
|
||||
if whence == os.SEEK_SET && hrs.rc == nil {
|
||||
// If no request has been made yet, and we are seeking to an
|
||||
// absolute position, set the read offset as well to avoid an
|
||||
// unnecessary request.
|
||||
hrs.readerOffset = offset
|
||||
}
|
||||
|
||||
_, err := hrs.reader()
|
||||
if err != nil {
|
||||
hrs.readerOffset = lastReaderOffset
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -101,14 +114,14 @@ func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
|
||||
switch whence {
|
||||
case os.SEEK_CUR:
|
||||
newOffset += int64(offset)
|
||||
newOffset += offset
|
||||
case os.SEEK_END:
|
||||
if hrs.size < 0 {
|
||||
return 0, errors.New("content length not known")
|
||||
}
|
||||
newOffset = hrs.size + int64(offset)
|
||||
newOffset = hrs.size + offset
|
||||
case os.SEEK_SET:
|
||||
newOffset = int64(offset)
|
||||
newOffset = offset
|
||||
}
|
||||
|
||||
if newOffset < 0 {
|
||||
@@ -131,7 +144,6 @@ func (hrs *httpReadSeeker) Close() error {
|
||||
}
|
||||
|
||||
hrs.rc = nil
|
||||
hrs.brd = nil
|
||||
|
||||
hrs.err = errors.New("httpLayer: closed")
|
||||
|
||||
@@ -154,7 +166,7 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||
}
|
||||
|
||||
if hrs.rc != nil {
|
||||
return hrs.brd, nil
|
||||
return hrs.rc, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", hrs.url, nil)
|
||||
@@ -163,10 +175,8 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||
}
|
||||
|
||||
if hrs.readerOffset > 0 {
|
||||
// TODO(stevvooe): Get this working correctly.
|
||||
|
||||
// If we are at different offset, issue a range request from there.
|
||||
req.Header.Add("Range", "1-")
|
||||
req.Header.Add("Range", fmt.Sprintf("bytes=%d-", hrs.readerOffset))
|
||||
// TODO: get context in here
|
||||
// context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range"))
|
||||
}
|
||||
@@ -179,12 +189,55 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||
// Normally would use client.SuccessStatus, but that would be a cyclic
|
||||
// import
|
||||
if resp.StatusCode >= 200 && resp.StatusCode <= 399 {
|
||||
hrs.rc = resp.Body
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
if hrs.readerOffset > 0 {
|
||||
if resp.StatusCode != http.StatusPartialContent {
|
||||
return nil, ErrWrongCodeForByteRange
|
||||
}
|
||||
|
||||
contentRange := resp.Header.Get("Content-Range")
|
||||
if contentRange == "" {
|
||||
return nil, errors.New("no Content-Range header found in HTTP 206 response")
|
||||
}
|
||||
|
||||
submatches := contentRangeRegexp.FindStringSubmatch(contentRange)
|
||||
if len(submatches) < 4 {
|
||||
return nil, fmt.Errorf("could not parse Content-Range header: %s", contentRange)
|
||||
}
|
||||
|
||||
startByte, err := strconv.ParseUint(submatches[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse start of range in Content-Range header: %s", contentRange)
|
||||
}
|
||||
|
||||
if startByte != uint64(hrs.readerOffset) {
|
||||
return nil, fmt.Errorf("received Content-Range starting at offset %d instead of requested %d", startByte, hrs.readerOffset)
|
||||
}
|
||||
|
||||
endByte, err := strconv.ParseUint(submatches[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse end of range in Content-Range header: %s", contentRange)
|
||||
}
|
||||
|
||||
if submatches[3] == "*" {
|
||||
hrs.size = -1
|
||||
} else {
|
||||
size, err := strconv.ParseUint(submatches[3], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse total size in Content-Range header: %s", contentRange)
|
||||
}
|
||||
|
||||
if endByte+1 != size {
|
||||
return nil, fmt.Errorf("range in Content-Range stops before the end of the content: %s", contentRange)
|
||||
}
|
||||
|
||||
hrs.size = int64(size)
|
||||
}
|
||||
} else if resp.StatusCode == http.StatusOK {
|
||||
hrs.size = resp.ContentLength
|
||||
} else {
|
||||
hrs.size = -1
|
||||
}
|
||||
hrs.rc = resp.Body
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
if hrs.errorHandler != nil {
|
||||
@@ -193,11 +246,5 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||
return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status)
|
||||
}
|
||||
|
||||
if hrs.brd == nil {
|
||||
hrs.brd = bufio.NewReader(hrs.rc)
|
||||
} else {
|
||||
hrs.brd.Reset(hrs.rc)
|
||||
}
|
||||
|
||||
return hrs.brd, nil
|
||||
return hrs.rc, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user