소스 검색

Merge pull request #28235 from dmcgowan/fix-registry-authorization-errors

Fix registry authorization errors
Tõnis Tiigi 8 년 전
부모
커밋
48a0c3e831

+ 5 - 4
cli/command/image/trust.go

@@ -20,6 +20,7 @@ import (
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/distribution/digest"
 	"github.com/docker/distribution/digest"
 	"github.com/docker/distribution/registry/client/auth"
 	"github.com/docker/distribution/registry/client/auth"
+	"github.com/docker/distribution/registry/client/auth/challenge"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	registrytypes "github.com/docker/docker/api/types/registry"
 	registrytypes "github.com/docker/docker/api/types/registry"
@@ -291,7 +292,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
 		}
 		}
 		fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest)
 		fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest)
 
 
-		ref, err := reference.WithDigest(repoInfo, r.digest)
+		ref, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
@@ -305,7 +306,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
-			trustedRef, err := reference.WithDigest(repoInfo, r.digest)
+			trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}
@@ -434,7 +435,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	challengeManager := auth.NewSimpleChallengeManager()
+	challengeManager := challenge.NewSimpleManager()
 
 
 	resp, err := pingClient.Do(req)
 	resp, err := pingClient.Do(req)
 	if err != nil {
 	if err != nil {
@@ -523,7 +524,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference
 
 
 	}
 	}
 
 
-	return reference.WithDigest(ref, r.digest)
+	return reference.WithDigest(reference.TrimNamed(ref), r.digest)
 }
 }
 
 
 func convertTarget(t client.Target) (target, error) {
 func convertTarget(t client.Target) (target, error) {

+ 1 - 1
daemon/image_pull.go

@@ -33,7 +33,7 @@ func (daemon *Daemon) PullImage(ctx context.Context, image, tag string, metaHead
 		var dgst digest.Digest
 		var dgst digest.Digest
 		dgst, err = digest.ParseDigest(tag)
 		dgst, err = digest.ParseDigest(tag)
 		if err == nil {
 		if err == nil {
-			ref, err = reference.WithDigest(ref, dgst)
+			ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
 		} else {
 		} else {
 			ref, err = reference.WithTag(ref, tag)
 			ref, err = reference.WithTag(ref, tag)
 		}
 		}

+ 34 - 0
distribution/errors.go

@@ -5,11 +5,14 @@ import (
 	"strings"
 	"strings"
 	"syscall"
 	"syscall"
 
 
+	"github.com/Sirupsen/logrus"
 	"github.com/docker/distribution/registry/api/errcode"
 	"github.com/docker/distribution/registry/api/errcode"
 	"github.com/docker/distribution/registry/api/v2"
 	"github.com/docker/distribution/registry/api/v2"
 	"github.com/docker/distribution/registry/client"
 	"github.com/docker/distribution/registry/client"
 	"github.com/docker/distribution/registry/client/auth"
 	"github.com/docker/distribution/registry/client/auth"
 	"github.com/docker/docker/distribution/xfer"
 	"github.com/docker/docker/distribution/xfer"
+	"github.com/docker/docker/reference"
+	"github.com/pkg/errors"
 )
 )
 
 
 // ErrNoSupport is an error type used for errors indicating that an operation
 // ErrNoSupport is an error type used for errors indicating that an operation
@@ -56,6 +59,37 @@ func shouldV2Fallback(err errcode.Error) bool {
 	return false
 	return false
 }
 }
 
 
+func translatePullError(err error, ref reference.Named) error {
+	switch v := err.(type) {
+	case errcode.Errors:
+		if len(v) != 0 {
+			for _, extra := range v[1:] {
+				logrus.Infof("Ignoring extra error returned from registry: %v", extra)
+			}
+			return translatePullError(v[0], ref)
+		}
+	case errcode.Error:
+		var newErr error
+		switch v.Code {
+		case errcode.ErrorCodeDenied:
+			// ErrorCodeDenied is used when access to the repository was denied
+			newErr = errors.Errorf("repository %s not found: does not exist or no read access", ref.Name())
+		case v2.ErrorCodeManifestUnknown:
+			newErr = errors.Errorf("manifest for %s not found", ref.String())
+		case v2.ErrorCodeNameUnknown:
+			newErr = errors.Errorf("repository %s not found", ref.Name())
+		}
+		if newErr != nil {
+			logrus.Infof("Translating %q to %q", err, newErr)
+			return newErr
+		}
+	case xfer.DoNotRetry:
+		return translatePullError(v.Err, ref)
+	}
+
+	return err
+}
+
 // continueOnError returns true if we should fallback to the next endpoint
 // continueOnError returns true if we should fallback to the next endpoint
 // as a result of this error.
 // as a result of this error.
 func continueOnError(err error) bool {
 func continueOnError(err error) bool {

+ 3 - 3
distribution/pull.go

@@ -168,7 +168,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
 				continue
 				continue
 			}
 			}
 			logrus.Errorf("Not continuing with pull after error: %v", err)
 			logrus.Errorf("Not continuing with pull after error: %v", err)
-			return err
+			return translatePullError(err, ref)
 		}
 		}
 
 
 		imagePullConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "pull")
 		imagePullConfig.ImageEventLogger(ref.String(), repoInfo.Name(), "pull")
@@ -179,7 +179,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
 		lastErr = fmt.Errorf("no endpoints found for %s", ref.String())
 		lastErr = fmt.Errorf("no endpoints found for %s", ref.String())
 	}
 	}
 
 
-	return lastErr
+	return translatePullError(lastErr, ref)
 }
 }
 
 
 // writeStatus writes a status message to out. If layersDownloaded is true, the
 // writeStatus writes a status message to out. If layersDownloaded is true, the
@@ -206,7 +206,7 @@ func ValidateRepoName(name string) error {
 }
 }
 
 
 func addDigestReference(store reference.Store, ref reference.Named, dgst digest.Digest, id digest.Digest) error {
 func addDigestReference(store reference.Store, ref reference.Named, dgst digest.Digest, id digest.Digest) error {
-	dgstRef, err := reference.WithDigest(ref, dgst)
+	dgstRef, err := reference.WithDigest(reference.TrimNamed(ref), dgst)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 1 - 1
distribution/pull_v2.go

@@ -671,7 +671,7 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf
 		return "", "", err
 		return "", "", err
 	}
 	}
 
 
-	manifestRef, err := reference.WithDigest(ref, manifestDigest)
+	manifestRef, err := reference.WithDigest(reference.TrimNamed(ref), manifestDigest)
 	if err != nil {
 	if err != nil {
 		return "", "", err
 		return "", "", err
 	}
 	}

+ 1 - 1
distribution/push_v2.go

@@ -331,7 +331,7 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress.
 				continue
 				continue
 			}
 			}
 
 
-			canonicalRef, err := distreference.WithDigest(remoteRef, mountCandidate.Digest)
+			canonicalRef, err := distreference.WithDigest(distreference.TrimNamed(remoteRef), mountCandidate.Digest)
 			if err != nil {
 			if err != nil {
 				logrus.Errorf("failed to make canonical reference: %v", err)
 				logrus.Errorf("failed to make canonical reference: %v", err)
 				continue
 				continue

+ 1 - 1
integration-cli/docker_cli_by_digest_test.go

@@ -114,7 +114,7 @@ func testPullByDigestNoFallback(c *check.C) {
 	imageReference := fmt.Sprintf("%s@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", repoName)
 	imageReference := fmt.Sprintf("%s@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", repoName)
 	out, _, err := dockerCmdWithError("pull", imageReference)
 	out, _, err := dockerCmdWithError("pull", imageReference)
 	c.Assert(err, checker.NotNil, check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image"))
 	c.Assert(err, checker.NotNil, check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image"))
-	c.Assert(out, checker.Contains, "manifest unknown", check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image"))
+	c.Assert(out, checker.Contains, fmt.Sprintf("manifest for %s not found", imageReference), check.Commentf("expected non-zero exit status and correct error message when pulling non-existing image"))
 }
 }
 
 
 func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) {
 func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) {

+ 8 - 15
integration-cli/docker_cli_pull_test.go

@@ -48,12 +48,12 @@ func (s *DockerHubPullSuite) TestPullNonExistingImage(c *check.C) {
 	}
 	}
 
 
 	entries := []entry{
 	entries := []entry{
-		{"library/asdfasdf", "asdfasdf", "foobar"},
-		{"library/asdfasdf", "library/asdfasdf", "foobar"},
-		{"library/asdfasdf", "asdfasdf", ""},
-		{"library/asdfasdf", "asdfasdf", "latest"},
-		{"library/asdfasdf", "library/asdfasdf", ""},
-		{"library/asdfasdf", "library/asdfasdf", "latest"},
+		{"asdfasdf", "asdfasdf", "foobar"},
+		{"asdfasdf", "library/asdfasdf", "foobar"},
+		{"asdfasdf", "asdfasdf", ""},
+		{"asdfasdf", "asdfasdf", "latest"},
+		{"asdfasdf", "library/asdfasdf", ""},
+		{"asdfasdf", "library/asdfasdf", "latest"},
 	}
 	}
 
 
 	// The option field indicates "-a" or not.
 	// The option field indicates "-a" or not.
@@ -98,18 +98,11 @@ func (s *DockerHubPullSuite) TestPullNonExistingImage(c *check.C) {
 	for record := range recordChan {
 	for record := range recordChan {
 		if len(record.option) == 0 {
 		if len(record.option) == 0 {
 			c.Assert(record.err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", record.out))
 			c.Assert(record.err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", record.out))
-			// Hub returns 401 rather than 404 for nonexistent repos over
-			// the v2 protocol - but we should end up falling back to v1,
-			// which does return a 404.
-			tag := record.e.tag
-			if tag == "" {
-				tag = "latest"
-			}
-			c.Assert(record.out, checker.Contains, fmt.Sprintf("Error: image %s:%s not found", record.e.repo, tag), check.Commentf("expected image not found error messages"))
+			c.Assert(record.out, checker.Contains, fmt.Sprintf("repository %s not found: does not exist or no read access", record.e.repo), check.Commentf("expected image not found error messages"))
 		} else {
 		} else {
 			// pull -a on a nonexistent registry should fall back as well
 			// pull -a on a nonexistent registry should fall back as well
 			c.Assert(record.err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", record.out))
 			c.Assert(record.err, checker.NotNil, check.Commentf("expected non-zero exit status when pulling non-existing image: %s", record.out))
-			c.Assert(record.out, checker.Contains, fmt.Sprintf("Error: image %s not found", record.e.repo), check.Commentf("expected image not found error messages"))
+			c.Assert(record.out, checker.Contains, fmt.Sprintf("repository %s not found", record.e.repo), check.Commentf("expected image not found error messages"))
 			c.Assert(record.out, checker.Not(checker.Contains), "unauthorized", check.Commentf(`message should not contain "unauthorized"`))
 			c.Assert(record.out, checker.Not(checker.Contains), "unauthorized", check.Commentf(`message should not contain "unauthorized"`))
 		}
 		}
 	}
 	}

+ 1 - 1
migrate/v1/migratev1.go

@@ -331,7 +331,7 @@ func migrateRefs(root, driverName string, rs refAdder, mappings map[string]image
 					continue
 					continue
 				}
 				}
 				if dgst, err := digest.ParseDigest(tag); err == nil {
 				if dgst, err := digest.ParseDigest(tag); err == nil {
-					canonical, err := reference.WithDigest(ref, dgst)
+					canonical, err := reference.WithDigest(reference.TrimNamed(ref), dgst)
 					if err != nil {
 					if err != nil {
 						logrus.Errorf("migrate tags: invalid digest %q, %q", dgst, err)
 						logrus.Errorf("migrate tags: invalid digest %q, %q", dgst, err)
 						continue
 						continue

+ 5 - 0
reference/reference.go

@@ -70,6 +70,11 @@ func ParseNamed(s string) (Named, error) {
 	return r, nil
 	return r, nil
 }
 }
 
 
+// TrimNamed removes any tag or digest from the named reference
+func TrimNamed(ref Named) Named {
+	return &namedRef{distreference.TrimNamed(ref)}
+}
+
 // WithName returns a named object representing the given string. If the input
 // WithName returns a named object representing the given string. If the input
 // is invalid ErrReferenceInvalidFormat will be returned.
 // is invalid ErrReferenceInvalidFormat will be returned.
 func WithName(name string) (Named, error) {
 func WithName(name string) (Named, error) {

+ 3 - 2
registry/auth.go

@@ -10,6 +10,7 @@ import (
 
 
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/distribution/registry/client/auth"
 	"github.com/docker/distribution/registry/client/auth"
+	"github.com/docker/distribution/registry/client/auth/challenge"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
 	registrytypes "github.com/docker/docker/api/types/registry"
 	registrytypes "github.com/docker/docker/api/types/registry"
@@ -255,7 +256,7 @@ func (err PingResponseError) Error() string {
 // challenge manager for the supported authentication types and
 // challenge manager for the supported authentication types and
 // whether v2 was confirmed by the response. If a response is received but
 // whether v2 was confirmed by the response. If a response is received but
 // cannot be interpreted a PingResponseError will be returned.
 // cannot be interpreted a PingResponseError will be returned.
-func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
+func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, bool, error) {
 	var (
 	var (
 		foundV2   = false
 		foundV2   = false
 		v2Version = auth.APIVersion{
 		v2Version = auth.APIVersion{
@@ -291,7 +292,7 @@ func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (auth.Challe
 		}
 		}
 	}
 	}
 
 
-	challengeManager := auth.NewSimpleChallengeManager()
+	challengeManager := challenge.NewSimpleManager()
 	if err := challengeManager.AddResponse(resp); err != nil {
 	if err := challengeManager.AddResponse(resp); err != nil {
 		return nil, foundV2, PingResponseError{
 		return nil, foundV2, PingResponseError{
 			Err: err,
 			Err: err,

+ 1 - 1
vendor.conf

@@ -44,7 +44,7 @@ github.com/boltdb/bolt fff57c100f4dea1905678da7e90d92429dff2904
 github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
 github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
 
 
 # get graph and distribution packages
 # get graph and distribution packages
-github.com/docker/distribution c04791f441f98bcf073353d7317db83663cf3ea2
+github.com/docker/distribution 8016d2d8903e378edacac11e4d809efbc987ad61
 github.com/vbatts/tar-split v0.10.1
 github.com/vbatts/tar-split v0.10.1
 
 
 # get go-zfs packages
 # get go-zfs packages

+ 30 - 0
vendor/github.com/docker/distribution/reference/reference.go

@@ -24,6 +24,7 @@ package reference
 import (
 import (
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
+	"path"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/distribution/digest"
 	"github.com/docker/distribution/digest"
@@ -218,6 +219,13 @@ func WithTag(name Named, tag string) (NamedTagged, error) {
 	if !anchoredTagRegexp.MatchString(tag) {
 	if !anchoredTagRegexp.MatchString(tag) {
 		return nil, ErrTagInvalidFormat
 		return nil, ErrTagInvalidFormat
 	}
 	}
+	if canonical, ok := name.(Canonical); ok {
+		return reference{
+			name:   name.Name(),
+			tag:    tag,
+			digest: canonical.Digest(),
+		}, nil
+	}
 	return taggedReference{
 	return taggedReference{
 		name: name.Name(),
 		name: name.Name(),
 		tag:  tag,
 		tag:  tag,
@@ -230,12 +238,34 @@ func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
 	if !anchoredDigestRegexp.MatchString(digest.String()) {
 	if !anchoredDigestRegexp.MatchString(digest.String()) {
 		return nil, ErrDigestInvalidFormat
 		return nil, ErrDigestInvalidFormat
 	}
 	}
+	if tagged, ok := name.(Tagged); ok {
+		return reference{
+			name:   name.Name(),
+			tag:    tagged.Tag(),
+			digest: digest,
+		}, nil
+	}
 	return canonicalReference{
 	return canonicalReference{
 		name:   name.Name(),
 		name:   name.Name(),
 		digest: digest,
 		digest: digest,
 	}, nil
 	}, nil
 }
 }
 
 
+// Match reports whether ref matches the specified pattern.
+// See https://godoc.org/path#Match for supported patterns.
+func Match(pattern string, ref Reference) (bool, error) {
+	matched, err := path.Match(pattern, ref.String())
+	if namedRef, isNamed := ref.(Named); isNamed && !matched {
+		matched, _ = path.Match(pattern, namedRef.Name())
+	}
+	return matched, err
+}
+
+// TrimNamed removes any tag or digest from the named reference.
+func TrimNamed(ref Named) Named {
+	return repository(ref.Name())
+}
+
 func getBestReferenceType(ref reference) Reference {
 func getBestReferenceType(ref reference) Reference {
 	if ref.name == "" {
 	if ref.name == "" {
 		// Allow digest only references
 		// Allow digest only references

+ 161 - 0
vendor/github.com/docker/distribution/registry/api/v2/headerparser.go

@@ -0,0 +1,161 @@
+package v2
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+	"unicode"
+)
+
+var (
+	// according to rfc7230
+	reToken            = regexp.MustCompile(`^[^"(),/:;<=>?@[\]{}[:space:][:cntrl:]]+`)
+	reQuotedValue      = regexp.MustCompile(`^[^\\"]+`)
+	reEscapedCharacter = regexp.MustCompile(`^[[:blank:][:graph:]]`)
+)
+
+// parseForwardedHeader is a benevolent parser of Forwarded header defined in rfc7239. The header contains
+// a comma-separated list of forwarding key-value pairs. Each list element is set by single proxy. The
+// function parses only the first element of the list, which is set by the very first proxy. It returns a map
+// of corresponding key-value pairs and an unparsed slice of the input string.
+//
+// Examples of Forwarded header values:
+//
+//  1. Forwarded: For=192.0.2.43; Proto=https,For="[2001:db8:cafe::17]",For=unknown
+//  2. Forwarded: for="192.0.2.43:443"; host="registry.example.org", for="10.10.05.40:80"
+//
+// The first will be parsed into {"for": "192.0.2.43", "proto": "https"} while the second into
+// {"for": "192.0.2.43:443", "host": "registry.example.org"}.
+func parseForwardedHeader(forwarded string) (map[string]string, string, error) {
+	// Following are states of forwarded header parser. Any state could transition to a failure.
+	const (
+		// terminating state; can transition to Parameter
+		stateElement = iota
+		// terminating state; can transition to KeyValueDelimiter
+		stateParameter
+		// can transition to Value
+		stateKeyValueDelimiter
+		// can transition to one of { QuotedValue, PairEnd }
+		stateValue
+		// can transition to one of { EscapedCharacter, PairEnd }
+		stateQuotedValue
+		// can transition to one of { QuotedValue }
+		stateEscapedCharacter
+		// terminating state; can transition to one of { Parameter, Element }
+		statePairEnd
+	)
+
+	var (
+		parameter string
+		value     string
+		parse     = forwarded[:]
+		res       = map[string]string{}
+		state     = stateElement
+	)
+
+Loop:
+	for {
+		// skip spaces unless in quoted value
+		if state != stateQuotedValue && state != stateEscapedCharacter {
+			parse = strings.TrimLeftFunc(parse, unicode.IsSpace)
+		}
+
+		if len(parse) == 0 {
+			if state != stateElement && state != statePairEnd && state != stateParameter {
+				return nil, parse, fmt.Errorf("unexpected end of input")
+			}
+			// terminating
+			break
+		}
+
+		switch state {
+		// terminate at list element delimiter
+		case stateElement:
+			if parse[0] == ',' {
+				parse = parse[1:]
+				break Loop
+			}
+			state = stateParameter
+
+		// parse parameter (the key of key-value pair)
+		case stateParameter:
+			match := reToken.FindString(parse)
+			if len(match) == 0 {
+				return nil, parse, fmt.Errorf("failed to parse token at position %d", len(forwarded)-len(parse))
+			}
+			parameter = strings.ToLower(match)
+			parse = parse[len(match):]
+			state = stateKeyValueDelimiter
+
+		// parse '='
+		case stateKeyValueDelimiter:
+			if parse[0] != '=' {
+				return nil, parse, fmt.Errorf("expected '=', not '%c' at position %d", parse[0], len(forwarded)-len(parse))
+			}
+			parse = parse[1:]
+			state = stateValue
+
+		// parse value or quoted value
+		case stateValue:
+			if parse[0] == '"' {
+				parse = parse[1:]
+				state = stateQuotedValue
+			} else {
+				value = reToken.FindString(parse)
+				if len(value) == 0 {
+					return nil, parse, fmt.Errorf("failed to parse value at position %d", len(forwarded)-len(parse))
+				}
+				if _, exists := res[parameter]; exists {
+					return nil, parse, fmt.Errorf("duplicate parameter %q at position %d", parameter, len(forwarded)-len(parse))
+				}
+				res[parameter] = value
+				parse = parse[len(value):]
+				value = ""
+				state = statePairEnd
+			}
+
+		// parse a part of quoted value until the first backslash
+		case stateQuotedValue:
+			match := reQuotedValue.FindString(parse)
+			value += match
+			parse = parse[len(match):]
+			switch {
+			case len(parse) == 0:
+				return nil, parse, fmt.Errorf("unterminated quoted string")
+			case parse[0] == '"':
+				res[parameter] = value
+				value = ""
+				parse = parse[1:]
+				state = statePairEnd
+			case parse[0] == '\\':
+				parse = parse[1:]
+				state = stateEscapedCharacter
+			}
+
+		// parse escaped character in a quoted string, ignore the backslash
+		// transition back to QuotedValue state
+		case stateEscapedCharacter:
+			c := reEscapedCharacter.FindString(parse)
+			if len(c) == 0 {
+				return nil, parse, fmt.Errorf("invalid escape sequence at position %d", len(forwarded)-len(parse)-1)
+			}
+			value += c
+			parse = parse[1:]
+			state = stateQuotedValue
+
+		// expect either a new key-value pair, new list or end of input
+		case statePairEnd:
+			switch parse[0] {
+			case ';':
+				parse = parse[1:]
+				state = stateParameter
+			case ',':
+				state = stateElement
+			default:
+				return nil, parse, fmt.Errorf("expected ',' or ';', not %c at position %d", parse[0], len(forwarded)-len(parse))
+			}
+		}
+	}
+
+	return res, parse, nil
+}

+ 65 - 2
vendor/github.com/docker/distribution/registry/api/v2/urls.go

@@ -1,8 +1,10 @@
 package v2
 package v2
 
 
 import (
 import (
+	"net"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"strconv"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/distribution/reference"
 	"github.com/docker/distribution/reference"
@@ -49,10 +51,14 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
 	var scheme string
 	var scheme string
 
 
 	forwardedProto := r.Header.Get("X-Forwarded-Proto")
 	forwardedProto := r.Header.Get("X-Forwarded-Proto")
+	// TODO: log the error
+	forwardedHeader, _, _ := parseForwardedHeader(r.Header.Get("Forwarded"))
 
 
 	switch {
 	switch {
 	case len(forwardedProto) > 0:
 	case len(forwardedProto) > 0:
 		scheme = forwardedProto
 		scheme = forwardedProto
+	case len(forwardedHeader["proto"]) > 0:
+		scheme = forwardedHeader["proto"]
 	case r.TLS != nil:
 	case r.TLS != nil:
 		scheme = "https"
 		scheme = "https"
 	case len(r.URL.Scheme) > 0:
 	case len(r.URL.Scheme) > 0:
@@ -62,14 +68,46 @@ func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder {
 	}
 	}
 
 
 	host := r.Host
 	host := r.Host
-	forwardedHost := r.Header.Get("X-Forwarded-Host")
-	if len(forwardedHost) > 0 {
+
+	if forwardedHost := r.Header.Get("X-Forwarded-Host"); len(forwardedHost) > 0 {
 		// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
 		// According to the Apache mod_proxy docs, X-Forwarded-Host can be a
 		// comma-separated list of hosts, to which each proxy appends the
 		// comma-separated list of hosts, to which each proxy appends the
 		// requested host. We want to grab the first from this comma-separated
 		// requested host. We want to grab the first from this comma-separated
 		// list.
 		// list.
 		hosts := strings.SplitN(forwardedHost, ",", 2)
 		hosts := strings.SplitN(forwardedHost, ",", 2)
 		host = strings.TrimSpace(hosts[0])
 		host = strings.TrimSpace(hosts[0])
+	} else if addr, exists := forwardedHeader["for"]; exists {
+		host = addr
+	} else if h, exists := forwardedHeader["host"]; exists {
+		host = h
+	}
+
+	portLessHost, port := host, ""
+	if !isIPv6Address(portLessHost) {
+		// with go 1.6, this would treat the last part of IPv6 address as a port
+		portLessHost, port, _ = net.SplitHostPort(host)
+	}
+	if forwardedPort := r.Header.Get("X-Forwarded-Port"); len(port) == 0 && len(forwardedPort) > 0 {
+		ports := strings.SplitN(forwardedPort, ",", 2)
+		forwardedPort = strings.TrimSpace(ports[0])
+		if _, err := strconv.ParseInt(forwardedPort, 10, 32); err == nil {
+			port = forwardedPort
+		}
+	}
+
+	if len(portLessHost) > 0 {
+		host = portLessHost
+	}
+	if len(port) > 0 {
+		// remove enclosing brackets of ipv6 address otherwise they will be duplicated
+		if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
+			host = host[1 : len(host)-1]
+		}
+		// JoinHostPort properly encloses ipv6 addresses in square brackets
+		host = net.JoinHostPort(host, port)
+	} else if isIPv6Address(host) && host[0] != '[' {
+		// ipv6 needs to be enclosed in square brackets in urls
+		host = "[" + host + "]"
 	}
 	}
 
 
 	basePath := routeDescriptorsMap[RouteNameBase].Path
 	basePath := routeDescriptorsMap[RouteNameBase].Path
@@ -249,3 +287,28 @@ func appendValues(u string, values ...url.Values) string {
 
 
 	return appendValuesURL(up, values...).String()
 	return appendValuesURL(up, values...).String()
 }
 }
+
+// isIPv6Address returns true if given string is a valid IPv6 address. No port is allowed. The address may be
+// enclosed in square brackets.
+func isIPv6Address(host string) bool {
+	if len(host) > 1 && host[0] == '[' && host[len(host)-1] == ']' {
+		host = host[1 : len(host)-1]
+	}
+	// The IPv6 scoped addressing zone identifier starts after the last percent sign.
+	if i := strings.LastIndexByte(host, '%'); i > 0 {
+		host = host[:i]
+	}
+	ip := net.ParseIP(host)
+	if ip == nil {
+		return false
+	}
+	if ip.To16() == nil {
+		return false
+	}
+	if ip.To4() == nil {
+		return true
+	}
+	// dot can be present in ipv4-mapped address, it needs to come after a colon though
+	i := strings.IndexAny(host, ":.")
+	return i >= 0 && host[i] == ':'
+}

+ 1 - 1
vendor/github.com/docker/distribution/registry/client/auth/addr.go → vendor/github.com/docker/distribution/registry/client/auth/challenge/addr.go

@@ -1,4 +1,4 @@
-package auth
+package challenge
 
 
 import (
 import (
 	"net/url"
 	"net/url"

+ 10 - 10
vendor/github.com/docker/distribution/registry/client/auth/authchallenge.go → vendor/github.com/docker/distribution/registry/client/auth/challenge/authchallenge.go

@@ -1,4 +1,4 @@
-package auth
+package challenge
 
 
 import (
 import (
 	"fmt"
 	"fmt"
@@ -18,12 +18,12 @@ type Challenge struct {
 	Parameters map[string]string
 	Parameters map[string]string
 }
 }
 
 
-// ChallengeManager manages the challenges for endpoints.
+// Manager manages the challenges for endpoints.
 // The challenges are pulled out of HTTP responses. Only
 // The challenges are pulled out of HTTP responses. Only
 // responses which expect challenges should be added to
 // responses which expect challenges should be added to
 // the manager, since a non-unauthorized request will be
 // the manager, since a non-unauthorized request will be
 // viewed as not requiring challenges.
 // viewed as not requiring challenges.
-type ChallengeManager interface {
+type Manager interface {
 	// GetChallenges returns the challenges for the given
 	// GetChallenges returns the challenges for the given
 	// endpoint URL.
 	// endpoint URL.
 	GetChallenges(endpoint url.URL) ([]Challenge, error)
 	GetChallenges(endpoint url.URL) ([]Challenge, error)
@@ -37,19 +37,19 @@ type ChallengeManager interface {
 	AddResponse(resp *http.Response) error
 	AddResponse(resp *http.Response) error
 }
 }
 
 
-// NewSimpleChallengeManager returns an instance of
-// ChallengeManger which only maps endpoints to challenges
+// NewSimpleManager returns an instance of
+// Manger which only maps endpoints to challenges
 // based on the responses which have been added the
 // based on the responses which have been added the
 // manager. The simple manager will make no attempt to
 // manager. The simple manager will make no attempt to
 // perform requests on the endpoints or cache the responses
 // perform requests on the endpoints or cache the responses
 // to a backend.
 // to a backend.
-func NewSimpleChallengeManager() ChallengeManager {
-	return &simpleChallengeManager{
+func NewSimpleManager() Manager {
+	return &simpleManager{
 		Challanges: make(map[string][]Challenge),
 		Challanges: make(map[string][]Challenge),
 	}
 	}
 }
 }
 
 
-type simpleChallengeManager struct {
+type simpleManager struct {
 	sync.RWMutex
 	sync.RWMutex
 	Challanges map[string][]Challenge
 	Challanges map[string][]Challenge
 }
 }
@@ -59,7 +59,7 @@ func normalizeURL(endpoint *url.URL) {
 	endpoint.Host = canonicalAddr(endpoint)
 	endpoint.Host = canonicalAddr(endpoint)
 }
 }
 
 
-func (m *simpleChallengeManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
+func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) {
 	normalizeURL(&endpoint)
 	normalizeURL(&endpoint)
 
 
 	m.RLock()
 	m.RLock()
@@ -68,7 +68,7 @@ func (m *simpleChallengeManager) GetChallenges(endpoint url.URL) ([]Challenge, e
 	return challenges, nil
 	return challenges, nil
 }
 }
 
 
-func (m *simpleChallengeManager) AddResponse(resp *http.Response) error {
+func (m *simpleManager) AddResponse(resp *http.Response) error {
 	challenges := ResponseChallenges(resp)
 	challenges := ResponseChallenges(resp)
 	if resp.Request == nil {
 	if resp.Request == nil {
 		return fmt.Errorf("missing request reference")
 		return fmt.Errorf("missing request reference")

+ 6 - 5
vendor/github.com/docker/distribution/registry/client/auth/session.go

@@ -12,6 +12,7 @@ import (
 
 
 	"github.com/Sirupsen/logrus"
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/distribution/registry/client"
 	"github.com/docker/distribution/registry/client"
+	"github.com/docker/distribution/registry/client/auth/challenge"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/distribution/registry/client/transport"
 )
 )
 
 
@@ -58,7 +59,7 @@ type CredentialStore interface {
 // schemes. The handlers are tried in order, the higher priority authentication
 // schemes. The handlers are tried in order, the higher priority authentication
 // methods should be first. The challengeMap holds a list of challenges for
 // methods should be first. The challengeMap holds a list of challenges for
 // a given root API endpoint (for example "https://registry-1.docker.io/v2/").
 // a given root API endpoint (for example "https://registry-1.docker.io/v2/").
-func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler) transport.RequestModifier {
+func NewAuthorizer(manager challenge.Manager, handlers ...AuthenticationHandler) transport.RequestModifier {
 	return &endpointAuthorizer{
 	return &endpointAuthorizer{
 		challenges: manager,
 		challenges: manager,
 		handlers:   handlers,
 		handlers:   handlers,
@@ -66,7 +67,7 @@ func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler)
 }
 }
 
 
 type endpointAuthorizer struct {
 type endpointAuthorizer struct {
-	challenges ChallengeManager
+	challenges challenge.Manager
 	handlers   []AuthenticationHandler
 	handlers   []AuthenticationHandler
 	transport  http.RoundTripper
 	transport  http.RoundTripper
 }
 }
@@ -94,11 +95,11 @@ func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
 
 
 	if len(challenges) > 0 {
 	if len(challenges) > 0 {
 		for _, handler := range ea.handlers {
 		for _, handler := range ea.handlers {
-			for _, challenge := range challenges {
-				if challenge.Scheme != handler.Scheme() {
+			for _, c := range challenges {
+				if c.Scheme != handler.Scheme() {
 					continue
 					continue
 				}
 				}
-				if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
+				if err := handler.AuthorizeRequest(req, c.Parameters); err != nil {
 					return err
 					return err
 				}
 				}
 			}
 			}

+ 37 - 5
vendor/github.com/docker/distribution/registry/client/errors.go

@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"net/http"
 
 
 	"github.com/docker/distribution/registry/api/errcode"
 	"github.com/docker/distribution/registry/api/errcode"
+	"github.com/docker/distribution/registry/client/auth/challenge"
 )
 )
 
 
 // ErrNoErrorsInBody is returned when an HTTP response body parses to an empty
 // ErrNoErrorsInBody is returned when an HTTP response body parses to an empty
@@ -82,21 +83,52 @@ func parseHTTPErrorResponse(statusCode int, r io.Reader) error {
 	return errors
 	return errors
 }
 }
 
 
+func makeErrorList(err error) []error {
+	if errL, ok := err.(errcode.Errors); ok {
+		return []error(errL)
+	}
+	return []error{err}
+}
+
+func mergeErrors(err1, err2 error) error {
+	return errcode.Errors(append(makeErrorList(err1), makeErrorList(err2)...))
+}
+
 // HandleErrorResponse returns error parsed from HTTP response for an
 // HandleErrorResponse returns error parsed from HTTP response for an
 // unsuccessful HTTP response code (in the range 400 - 499 inclusive). An
 // unsuccessful HTTP response code (in the range 400 - 499 inclusive). An
 // UnexpectedHTTPStatusError returned for response code outside of expected
 // UnexpectedHTTPStatusError returned for response code outside of expected
 // range.
 // range.
 func HandleErrorResponse(resp *http.Response) error {
 func HandleErrorResponse(resp *http.Response) error {
-	if resp.StatusCode == 401 {
+	if resp.StatusCode >= 400 && resp.StatusCode < 500 {
+		// Check for OAuth errors within the `WWW-Authenticate` header first
+		// See https://tools.ietf.org/html/rfc6750#section-3
+		for _, c := range challenge.ResponseChallenges(resp) {
+			if c.Scheme == "bearer" {
+				var err errcode.Error
+				// codes defined at https://tools.ietf.org/html/rfc6750#section-3.1
+				switch c.Parameters["error"] {
+				case "invalid_token":
+					err.Code = errcode.ErrorCodeUnauthorized
+				case "insufficient_scope":
+					err.Code = errcode.ErrorCodeDenied
+				default:
+					continue
+				}
+				if description := c.Parameters["error_description"]; description != "" {
+					err.Message = description
+				} else {
+					err.Message = err.Code.Message()
+				}
+
+				return mergeErrors(err, parseHTTPErrorResponse(resp.StatusCode, resp.Body))
+			}
+		}
 		err := parseHTTPErrorResponse(resp.StatusCode, resp.Body)
 		err := parseHTTPErrorResponse(resp.StatusCode, resp.Body)
-		if uErr, ok := err.(*UnexpectedHTTPResponseError); ok {
+		if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 {
 			return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
 			return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response)
 		}
 		}
 		return err
 		return err
 	}
 	}
-	if resp.StatusCode >= 400 && resp.StatusCode < 500 {
-		return parseHTTPErrorResponse(resp.StatusCode, resp.Body)
-	}
 	return &UnexpectedHTTPStatusError{Status: resp.Status}
 	return &UnexpectedHTTPStatusError{Status: resp.Status}
 }
 }