diff --git a/registry/authchallenge.go b/registry/authchallenge.go index 5dc7d48d..9fe21508 100644 --- a/registry/authchallenge.go +++ b/registry/authchallenge.go @@ -1,6 +1,7 @@ package registry import ( + "errors" "fmt" "net/http" "net/url" @@ -12,6 +13,9 @@ var ( bearerRegex = regexp.MustCompile( `^\s*Bearer\s+(.*)$`) basicRegex = regexp.MustCompile(`^\s*Basic\s+.*$`) + + // ErrBasicAuth indicates that the repository requires basic rather than token authentication. + ErrBasicAuth = errors.New("basic auth required") ) func parseAuthHeader(header http.Header) (*authService, error) { @@ -25,7 +29,7 @@ func parseAuthHeader(header http.Header) (*authService, error) { func parseChallenge(challengeHeader string) (*authService, error) { if basicRegex.MatchString(challengeHeader) { - return nil, fmt.Errorf("basic auth required") + return nil, ErrBasicAuth } match := bearerRegex.FindAllStringSubmatch(challengeHeader, -1) diff --git a/registry/authchallenge_test.go b/registry/authchallenge_test.go index ff7ea768..2218f933 100644 --- a/registry/authchallenge_test.go +++ b/registry/authchallenge_test.go @@ -47,6 +47,14 @@ func TestParseChallenge(t *testing.T) { scope: []string{"repository:chrome:pull"}, }, }, + { + header: `Basic realm="https://r.j3ss.co/auth",service="Docker registry"`, + errorString: "basic auth required", + }, + { + header: `Basic realm="Registry Realm",service="Docker registry"`, + errorString: "basic auth required", + }, } for _, tc := range challengeHeaderCases { @@ -54,7 +62,7 @@ func TestParseChallenge(t *testing.T) { if err != nil && !strings.Contains(err.Error(), tc.errorString) { t.Fatalf("expected error to contain %v, got %s", tc.errorString, err) } - if !tc.value.equalTo(val) { + if err == nil && !tc.value.equalTo(val) { t.Fatalf("got %v, expected %v", val, tc.value) } diff --git a/registry/tokentransport.go b/registry/tokentransport.go index d1cb8712..6a0265cd 100644 --- a/registry/tokentransport.go +++ b/registry/tokentransport.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "net/url" - "strings" ) // TokenTransport defines the data structure for authentication via tokens. @@ -121,7 +120,8 @@ func isTokenDemand(resp *http.Response) (*authService, error) { return parseAuthHeader(resp.Header) } -// Token returns the required token for the specific resource url. +// Token returns the required token for the specific resource url. If the registry requires basic authentication, this +// function returns ErrBasicAuth. func (r *Registry) Token(url string) (string, error) { r.Logf("registry.token url=%s", url) @@ -189,16 +189,12 @@ func (r *Registry) Headers(uri string) (map[string]string, error) { // Get the token. token, err := r.Token(uri) if err != nil { - // If we get an error here of type: malformed auth challenge header: 'Basic realm="Registry Realm"' - // We need to use basic auth for the registry. - if !strings.Contains(err.Error(), `malformed auth challenge header: 'Basic realm="Registry Realm"'`) && !strings.Contains(err.Error(), "basic auth required") { - return nil, err + if err == ErrBasicAuth { + // If we couldn't get a token because the server requires basic auth, just return basic auth headers. + return map[string]string{ + "Authorization": fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(r.Username+":"+r.Password))), + }, nil } - - // Return basic auth headers. - return map[string]string{ - "Authorization": fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(r.Username+":"+r.Password))), - }, nil } if len(token) < 1 { diff --git a/registry/tokentransport_test.go b/registry/tokentransport_test.go new file mode 100644 index 00000000..6b6e00b3 --- /dev/null +++ b/registry/tokentransport_test.go @@ -0,0 +1,38 @@ +package registry + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/docker/docker/api/types" +) + +func TestErrBasicAuth(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set("www-authenticate", `Basic realm="Registry Realm",service="Docker registry"`) + w.WriteHeader(http.StatusUnauthorized) + } else { + w.WriteHeader(http.StatusOK) + } + })) + defer ts.Close() + + authConfig := types.AuthConfig{ + Username: "j3ss", + Password: "ss3j", + ServerAddress: ts.URL, + } + r, err := New(authConfig, Opt{Insecure: true, Debug: true}) + if err != nil { + t.Fatalf("expected no error creating client, got %v", err) + } + token, err := r.Token(ts.URL) + if err != ErrBasicAuth { + t.Fatalf("expected ErrBasicAuth getting token, got %v", err) + } + if token != "" { + t.Fatalf("expected empty token, got %v", err) + } +}