Use a distinguished error value for basic auth. (#120)

Rather than returning an error that requires pattern-matching from
`parseChallenge` when the challenge header requires basic
authentication, return a distinguished error value. This makes checking
for this error a bit easier.

This commit also updates the check in `r.Headers` to use the new error
value and adds a couple of regression tests.
This commit is contained in:
Pat Gavlin 2018-07-26 14:13:58 -07:00 committed by Jess Frazelle
parent 8e3ba57e3e
commit 65b2c0329d
4 changed files with 59 additions and 13 deletions

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
}
}