Browse code

Merge pull request #20832 from aaronlehmann/login-endpoint-refactor

Update login to use token handling code from distribution

Sebastiaan van Stijn authored on 2016/03/04 03:04:42
Showing 17 changed files
... ...
@@ -88,7 +88,7 @@ func Pull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullCo
88 88
 		return err
89 89
 	}
90 90
 
91
-	endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo)
91
+	endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo.Hostname())
92 92
 	if err != nil {
93 93
 		return err
94 94
 	}
... ...
@@ -100,7 +100,7 @@ func Push(ctx context.Context, ref reference.Named, imagePushConfig *ImagePushCo
100 100
 		return err
101 101
 	}
102 102
 
103
-	endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo)
103
+	endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo.Hostname())
104 104
 	if err != nil {
105 105
 		return err
106 106
 	}
... ...
@@ -5,7 +5,6 @@ import (
5 5
 	"net"
6 6
 	"net/http"
7 7
 	"net/url"
8
-	"strings"
9 8
 	"time"
10 9
 
11 10
 	"github.com/docker/distribution"
... ...
@@ -53,48 +52,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
53 53
 
54 54
 	modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), metaHeaders)
55 55
 	authTransport := transport.NewTransport(base, modifiers...)
56
-	pingClient := &http.Client{
57
-		Transport: authTransport,
58
-		Timeout:   15 * time.Second,
59
-	}
60
-	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
61
-	req, err := http.NewRequest("GET", endpointStr, nil)
62
-	if err != nil {
63
-		return nil, false, fallbackError{err: err}
64
-	}
65
-	resp, err := pingClient.Do(req)
66
-	if err != nil {
67
-		return nil, false, fallbackError{err: err}
68
-	}
69
-	defer resp.Body.Close()
70
-
71
-	// We got a HTTP request through, so we're using the right TLS settings.
72
-	// From this point forward, set transportOK to true in any fallbackError
73
-	// we return.
74 56
 
75
-	v2Version := auth.APIVersion{
76
-		Type:    "registry",
77
-		Version: "2.0",
78
-	}
79
-
80
-	versions := auth.APIVersions(resp, registry.DefaultRegistryVersionHeader)
81
-	for _, pingVersion := range versions {
82
-		if pingVersion == v2Version {
83
-			// The version header indicates we're definitely
84
-			// talking to a v2 registry. So don't allow future
85
-			// fallbacks to the v1 protocol.
86
-
87
-			foundVersion = true
88
-			break
57
+	challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)
58
+	if err != nil {
59
+		transportOK := false
60
+		if responseErr, ok := err.(registry.PingResponseError); ok {
61
+			transportOK = true
62
+			err = responseErr.Err
89 63
 		}
90
-	}
91
-
92
-	challengeManager := auth.NewSimpleChallengeManager()
93
-	if err := challengeManager.AddResponse(resp); err != nil {
94 64
 		return nil, foundVersion, fallbackError{
95 65
 			err:         err,
96 66
 			confirmedV2: foundVersion,
97
-			transportOK: true,
67
+			transportOK: transportOK,
98 68
 		}
99 69
 	}
100 70
 
... ...
@@ -106,20 +106,19 @@ func (s *DockerRegistrySuite) TestV1(c *check.C) {
106 106
 	defer cleanup()
107 107
 
108 108
 	s.d.Cmd("build", "--file", dockerfileName, ".")
109
-	c.Assert(v1Repo, check.Not(check.Equals), 0, check.Commentf("Expected v1 repository access after build"))
109
+	c.Assert(v1Repo, check.Equals, 1, check.Commentf("Expected v1 repository access after build"))
110 110
 
111 111
 	repoName := fmt.Sprintf("%s/busybox", reg.hostport)
112 112
 	s.d.Cmd("run", repoName)
113
-	c.Assert(v1Repo, check.Not(check.Equals), 1, check.Commentf("Expected v1 repository access after run"))
113
+	c.Assert(v1Repo, check.Equals, 2, check.Commentf("Expected v1 repository access after run"))
114 114
 
115 115
 	s.d.Cmd("login", "-u", "richard", "-p", "testtest", reg.hostport)
116
-	c.Assert(v1Logins, check.Not(check.Equals), 0, check.Commentf("Expected v1 login attempt"))
116
+	c.Assert(v1Logins, check.Equals, 1, check.Commentf("Expected v1 login attempt"))
117 117
 
118 118
 	s.d.Cmd("tag", "busybox", repoName)
119 119
 	s.d.Cmd("push", repoName)
120 120
 
121 121
 	c.Assert(v1Repo, check.Equals, 2)
122
-	c.Assert(v1Pings, check.Equals, 1)
123 122
 
124 123
 	s.d.Cmd("pull", repoName)
125 124
 	c.Assert(v1Repo, check.Equals, 3, check.Commentf("Expected v1 repository access after pull"))
... ...
@@ -4,28 +4,25 @@ import (
4 4
 	"fmt"
5 5
 	"io/ioutil"
6 6
 	"net/http"
7
+	"net/url"
7 8
 	"strings"
9
+	"time"
8 10
 
9 11
 	"github.com/Sirupsen/logrus"
12
+	"github.com/docker/distribution/registry/client/auth"
13
+	"github.com/docker/distribution/registry/client/transport"
10 14
 	"github.com/docker/engine-api/types"
11 15
 	registrytypes "github.com/docker/engine-api/types/registry"
12 16
 )
13 17
 
14
-// Login tries to register/login to the registry server.
15
-func Login(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
16
-	// Separates the v2 registry login logic from the v1 logic.
17
-	if registryEndpoint.Version == APIVersion2 {
18
-		return loginV2(authConfig, registryEndpoint, "" /* scope */)
18
+// loginV1 tries to register/login to the v1 registry server.
19
+func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) {
20
+	registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
21
+	if err != nil {
22
+		return "", err
19 23
 	}
20
-	return loginV1(authConfig, registryEndpoint)
21
-}
22 24
 
23
-// loginV1 tries to register/login to the v1 registry server.
24
-func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
25
-	var (
26
-		err           error
27
-		serverAddress = authConfig.ServerAddress
28
-	)
25
+	serverAddress := registryEndpoint.String()
29 26
 
30 27
 	logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
31 28
 
... ...
@@ -36,10 +33,16 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
36 36
 	loginAgainstOfficialIndex := serverAddress == IndexServer
37 37
 
38 38
 	req, err := http.NewRequest("GET", serverAddress+"users/", nil)
39
+	if err != nil {
40
+		return "", err
41
+	}
39 42
 	req.SetBasicAuth(authConfig.Username, authConfig.Password)
40 43
 	resp, err := registryEndpoint.client.Do(req)
41 44
 	if err != nil {
42
-		return "", err
45
+		// fallback when request could not be completed
46
+		return "", fallbackError{
47
+			err: err,
48
+		}
43 49
 	}
44 50
 	defer resp.Body.Close()
45 51
 	body, err := ioutil.ReadAll(resp.Body)
... ...
@@ -68,97 +71,82 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
68 68
 	}
69 69
 }
70 70
 
71
-// loginV2 tries to login to the v2 registry server. The given registry endpoint has been
72
-// pinged or setup with a list of authorization challenges. Each of these challenges are
73
-// tried until one of them succeeds. Currently supported challenge schemes are:
74
-// 		HTTP Basic Authorization
75
-// 		Token Authorization with a separate token issuing server
76
-// NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For
77
-// now, users should create their account through other means like directly from a web page
78
-// served by the v2 registry service provider. Whether this will be supported in the future
79
-// is to be determined.
80
-func loginV2(authConfig *types.AuthConfig, registryEndpoint *Endpoint, scope string) (string, error) {
81
-	logrus.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint)
82
-	var (
83
-		err       error
84
-		allErrors []error
85
-	)
86
-
87
-	for _, challenge := range registryEndpoint.AuthChallenges {
88
-		params := make(map[string]string, len(challenge.Parameters)+1)
89
-		for k, v := range challenge.Parameters {
90
-			params[k] = v
91
-		}
92
-		params["scope"] = scope
93
-		logrus.Debugf("trying %q auth challenge with params %v", challenge.Scheme, params)
94
-
95
-		switch strings.ToLower(challenge.Scheme) {
96
-		case "basic":
97
-			err = tryV2BasicAuthLogin(authConfig, params, registryEndpoint)
98
-		case "bearer":
99
-			err = tryV2TokenAuthLogin(authConfig, params, registryEndpoint)
100
-		default:
101
-			// Unsupported challenge types are explicitly skipped.
102
-			err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme)
103
-		}
104
-
105
-		if err == nil {
106
-			return "Login Succeeded", nil
107
-		}
71
+type loginCredentialStore struct {
72
+	authConfig *types.AuthConfig
73
+}
108 74
 
109
-		logrus.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err)
75
+func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
76
+	return lcs.authConfig.Username, lcs.authConfig.Password
77
+}
110 78
 
111
-		allErrors = append(allErrors, err)
112
-	}
79
+type fallbackError struct {
80
+	err error
81
+}
113 82
 
114
-	return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors)
83
+func (err fallbackError) Error() string {
84
+	return err.err.Error()
115 85
 }
116 86
 
117
-func tryV2BasicAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
118
-	req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
119
-	if err != nil {
120
-		return err
121
-	}
87
+// loginV2 tries to login to the v2 registry server. The given registry
88
+// endpoint will be pinged to get authorization challenges. These challenges
89
+// will be used to authenticate against the registry to validate credentials.
90
+func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) {
91
+	logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
122 92
 
123
-	req.SetBasicAuth(authConfig.Username, authConfig.Password)
93
+	modifiers := DockerHeaders(userAgent, nil)
94
+	authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
124 95
 
125
-	resp, err := registryEndpoint.client.Do(req)
96
+	challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
126 97
 	if err != nil {
127
-		return err
98
+		if !foundV2 {
99
+			err = fallbackError{err: err}
100
+		}
101
+		return "", err
128 102
 	}
129
-	defer resp.Body.Close()
130 103
 
131
-	if resp.StatusCode != http.StatusOK {
132
-		return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
104
+	creds := loginCredentialStore{
105
+		authConfig: authConfig,
133 106
 	}
134 107
 
135
-	return nil
136
-}
108
+	tokenHandler := auth.NewTokenHandler(authTransport, creds, "")
109
+	basicHandler := auth.NewBasicHandler(creds)
110
+	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
111
+	tr := transport.NewTransport(authTransport, modifiers...)
137 112
 
138
-func tryV2TokenAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
139
-	token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint)
140
-	if err != nil {
141
-		return err
113
+	loginClient := &http.Client{
114
+		Transport: tr,
115
+		Timeout:   15 * time.Second,
142 116
 	}
143 117
 
144
-	req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
118
+	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
119
+	req, err := http.NewRequest("GET", endpointStr, nil)
145 120
 	if err != nil {
146
-		return err
121
+		if !foundV2 {
122
+			err = fallbackError{err: err}
123
+		}
124
+		return "", err
147 125
 	}
148 126
 
149
-	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
150
-
151
-	resp, err := registryEndpoint.client.Do(req)
127
+	resp, err := loginClient.Do(req)
152 128
 	if err != nil {
153
-		return err
129
+		if !foundV2 {
130
+			err = fallbackError{err: err}
131
+		}
132
+		return "", err
154 133
 	}
155 134
 	defer resp.Body.Close()
156 135
 
157 136
 	if resp.StatusCode != http.StatusOK {
158
-		return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
137
+		// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
138
+		err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
139
+		if !foundV2 {
140
+			err = fallbackError{err: err}
141
+		}
142
+		return "", err
159 143
 	}
160 144
 
161
-	return nil
145
+	return "Login Succeeded", nil
146
+
162 147
 }
163 148
 
164 149
 // ResolveAuthConfig matches an auth configuration to a server address or a URL
... ...
@@ -193,3 +181,63 @@ func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registryt
193 193
 	// When all else fails, return an empty auth config
194 194
 	return types.AuthConfig{}
195 195
 }
196
+
197
+// PingResponseError is used when the response from a ping
198
+// was received but invalid.
199
+type PingResponseError struct {
200
+	Err error
201
+}
202
+
203
+func (err PingResponseError) Error() string {
204
+	return err.Error()
205
+}
206
+
207
+// PingV2Registry attempts to ping a v2 registry and on success return a
208
+// challenge manager for the supported authentication types and
209
+// whether v2 was confirmed by the response. If a response is received but
210
+// cannot be interpreted a PingResponseError will be returned.
211
+func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
212
+	var (
213
+		foundV2   = false
214
+		v2Version = auth.APIVersion{
215
+			Type:    "registry",
216
+			Version: "2.0",
217
+		}
218
+	)
219
+
220
+	pingClient := &http.Client{
221
+		Transport: transport,
222
+		Timeout:   15 * time.Second,
223
+	}
224
+	endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
225
+	req, err := http.NewRequest("GET", endpointStr, nil)
226
+	if err != nil {
227
+		return nil, false, err
228
+	}
229
+	resp, err := pingClient.Do(req)
230
+	if err != nil {
231
+		return nil, false, err
232
+	}
233
+	defer resp.Body.Close()
234
+
235
+	versions := auth.APIVersions(resp, DefaultRegistryVersionHeader)
236
+	for _, pingVersion := range versions {
237
+		if pingVersion == v2Version {
238
+			// The version header indicates we're definitely
239
+			// talking to a v2 registry. So don't allow future
240
+			// fallbacks to the v1 protocol.
241
+
242
+			foundV2 = true
243
+			break
244
+		}
245
+	}
246
+
247
+	challengeManager := auth.NewSimpleChallengeManager()
248
+	if err := challengeManager.AddResponse(resp); err != nil {
249
+		return nil, foundV2, PingResponseError{
250
+			Err: err,
251
+		}
252
+	}
253
+
254
+	return challengeManager, foundV2, nil
255
+}
196 256
deleted file mode 100644
... ...
@@ -1,150 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"net/http"
5
-	"strings"
6
-)
7
-
8
-// Octet types from RFC 2616.
9
-type octetType byte
10
-
11
-// AuthorizationChallenge carries information
12
-// from a WWW-Authenticate response header.
13
-type AuthorizationChallenge struct {
14
-	Scheme     string
15
-	Parameters map[string]string
16
-}
17
-
18
-var octetTypes [256]octetType
19
-
20
-const (
21
-	isToken octetType = 1 << iota
22
-	isSpace
23
-)
24
-
25
-func init() {
26
-	// OCTET      = <any 8-bit sequence of data>
27
-	// CHAR       = <any US-ASCII character (octets 0 - 127)>
28
-	// CTL        = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
29
-	// CR         = <US-ASCII CR, carriage return (13)>
30
-	// LF         = <US-ASCII LF, linefeed (10)>
31
-	// SP         = <US-ASCII SP, space (32)>
32
-	// HT         = <US-ASCII HT, horizontal-tab (9)>
33
-	// <">        = <US-ASCII double-quote mark (34)>
34
-	// CRLF       = CR LF
35
-	// LWS        = [CRLF] 1*( SP | HT )
36
-	// TEXT       = <any OCTET except CTLs, but including LWS>
37
-	// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
38
-	//              | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
39
-	// token      = 1*<any CHAR except CTLs or separators>
40
-	// qdtext     = <any TEXT except <">>
41
-
42
-	for c := 0; c < 256; c++ {
43
-		var t octetType
44
-		isCtl := c <= 31 || c == 127
45
-		isChar := 0 <= c && c <= 127
46
-		isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
47
-		if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
48
-			t |= isSpace
49
-		}
50
-		if isChar && !isCtl && !isSeparator {
51
-			t |= isToken
52
-		}
53
-		octetTypes[c] = t
54
-	}
55
-}
56
-
57
-func parseAuthHeader(header http.Header) []*AuthorizationChallenge {
58
-	var challenges []*AuthorizationChallenge
59
-	for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
60
-		v, p := parseValueAndParams(h)
61
-		if v != "" {
62
-			challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p})
63
-		}
64
-	}
65
-	return challenges
66
-}
67
-
68
-func parseValueAndParams(header string) (value string, params map[string]string) {
69
-	params = make(map[string]string)
70
-	value, s := expectToken(header)
71
-	if value == "" {
72
-		return
73
-	}
74
-	value = strings.ToLower(value)
75
-	s = "," + skipSpace(s)
76
-	for strings.HasPrefix(s, ",") {
77
-		var pkey string
78
-		pkey, s = expectToken(skipSpace(s[1:]))
79
-		if pkey == "" {
80
-			return
81
-		}
82
-		if !strings.HasPrefix(s, "=") {
83
-			return
84
-		}
85
-		var pvalue string
86
-		pvalue, s = expectTokenOrQuoted(s[1:])
87
-		if pvalue == "" {
88
-			return
89
-		}
90
-		pkey = strings.ToLower(pkey)
91
-		params[pkey] = pvalue
92
-		s = skipSpace(s)
93
-	}
94
-	return
95
-}
96
-
97
-func skipSpace(s string) (rest string) {
98
-	i := 0
99
-	for ; i < len(s); i++ {
100
-		if octetTypes[s[i]]&isSpace == 0 {
101
-			break
102
-		}
103
-	}
104
-	return s[i:]
105
-}
106
-
107
-func expectToken(s string) (token, rest string) {
108
-	i := 0
109
-	for ; i < len(s); i++ {
110
-		if octetTypes[s[i]]&isToken == 0 {
111
-			break
112
-		}
113
-	}
114
-	return s[:i], s[i:]
115
-}
116
-
117
-func expectTokenOrQuoted(s string) (value string, rest string) {
118
-	if !strings.HasPrefix(s, "\"") {
119
-		return expectToken(s)
120
-	}
121
-	s = s[1:]
122
-	for i := 0; i < len(s); i++ {
123
-		switch s[i] {
124
-		case '"':
125
-			return s[:i], s[i+1:]
126
-		case '\\':
127
-			p := make([]byte, len(s)-1)
128
-			j := copy(p, s[:i])
129
-			escape := true
130
-			for i = i + i; i < len(s); i++ {
131
-				b := s[i]
132
-				switch {
133
-				case escape:
134
-					escape = false
135
-					p[j] = b
136
-					j++
137
-				case b == '\\':
138
-					escape = true
139
-				case b == '"':
140
-					return string(p[:j]), s[i+1:]
141
-				default:
142
-					p[j] = b
143
-					j++
144
-				}
145
-			}
146
-			return "", ""
147
-		}
148
-	}
149
-	return "", ""
150
-}
... ...
@@ -49,6 +49,9 @@ var (
49 49
 	V2Only = false
50 50
 )
51 51
 
52
+// for mocking in unit tests
53
+var lookupIP = net.LookupIP
54
+
52 55
 // InstallFlags adds command-line options to the top-level flag parser for
53 56
 // the current process.
54 57
 func (options *Options) InstallFlags(cmd *flag.FlagSet, usageFn func(string) string) {
55 58
deleted file mode 100644
... ...
@@ -1,290 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"crypto/tls"
5
-	"encoding/json"
6
-	"fmt"
7
-	"io/ioutil"
8
-	"net"
9
-	"net/http"
10
-	"net/url"
11
-	"strings"
12
-
13
-	"github.com/Sirupsen/logrus"
14
-	"github.com/docker/distribution/registry/api/v2"
15
-	"github.com/docker/distribution/registry/client/transport"
16
-	registrytypes "github.com/docker/engine-api/types/registry"
17
-)
18
-
19
-// for mocking in unit tests
20
-var lookupIP = net.LookupIP
21
-
22
-// scans string for api version in the URL path. returns the trimmed address, if version found, string and API version.
23
-func scanForAPIVersion(address string) (string, APIVersion) {
24
-	var (
25
-		chunks        []string
26
-		apiVersionStr string
27
-	)
28
-
29
-	if strings.HasSuffix(address, "/") {
30
-		address = address[:len(address)-1]
31
-	}
32
-
33
-	chunks = strings.Split(address, "/")
34
-	apiVersionStr = chunks[len(chunks)-1]
35
-
36
-	for k, v := range apiVersions {
37
-		if apiVersionStr == v {
38
-			address = strings.Join(chunks[:len(chunks)-1], "/")
39
-			return address, k
40
-		}
41
-	}
42
-
43
-	return address, APIVersionUnknown
44
-}
45
-
46
-// NewEndpoint parses the given address to return a registry endpoint.  v can be used to
47
-// specify a specific endpoint version
48
-func NewEndpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header, v APIVersion) (*Endpoint, error) {
49
-	tlsConfig, err := newTLSConfig(index.Name, index.Secure)
50
-	if err != nil {
51
-		return nil, err
52
-	}
53
-
54
-	endpoint, err := newEndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders)
55
-	if err != nil {
56
-		return nil, err
57
-	}
58
-
59
-	if v != APIVersionUnknown {
60
-		endpoint.Version = v
61
-	}
62
-	if err := validateEndpoint(endpoint); err != nil {
63
-		return nil, err
64
-	}
65
-
66
-	return endpoint, nil
67
-}
68
-
69
-func validateEndpoint(endpoint *Endpoint) error {
70
-	logrus.Debugf("pinging registry endpoint %s", endpoint)
71
-
72
-	// Try HTTPS ping to registry
73
-	endpoint.URL.Scheme = "https"
74
-	if _, err := endpoint.Ping(); err != nil {
75
-		if endpoint.IsSecure {
76
-			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
77
-			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
78
-			return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
79
-		}
80
-
81
-		// If registry is insecure and HTTPS failed, fallback to HTTP.
82
-		logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
83
-		endpoint.URL.Scheme = "http"
84
-
85
-		var err2 error
86
-		if _, err2 = endpoint.Ping(); err2 == nil {
87
-			return nil
88
-		}
89
-
90
-		return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
91
-	}
92
-
93
-	return nil
94
-}
95
-
96
-func newEndpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*Endpoint, error) {
97
-	endpoint := &Endpoint{
98
-		IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify),
99
-		URL:      new(url.URL),
100
-		Version:  APIVersionUnknown,
101
-	}
102
-
103
-	*endpoint.URL = address
104
-
105
-	// TODO(tiborvass): make sure a ConnectTimeout transport is used
106
-	tr := NewTransport(tlsConfig)
107
-	endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(userAgent, metaHeaders)...))
108
-	return endpoint, nil
109
-}
110
-
111
-func newEndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*Endpoint, error) {
112
-	if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
113
-		address = "https://" + address
114
-	}
115
-
116
-	trimmedAddress, detectedVersion := scanForAPIVersion(address)
117
-
118
-	uri, err := url.Parse(trimmedAddress)
119
-	if err != nil {
120
-		return nil, err
121
-	}
122
-
123
-	endpoint, err := newEndpoint(*uri, tlsConfig, userAgent, metaHeaders)
124
-	if err != nil {
125
-		return nil, err
126
-	}
127
-
128
-	endpoint.Version = detectedVersion
129
-	return endpoint, nil
130
-}
131
-
132
-// Endpoint stores basic information about a registry endpoint.
133
-type Endpoint struct {
134
-	client         *http.Client
135
-	URL            *url.URL
136
-	Version        APIVersion
137
-	IsSecure       bool
138
-	AuthChallenges []*AuthorizationChallenge
139
-	URLBuilder     *v2.URLBuilder
140
-}
141
-
142
-// Get the formatted URL for the root of this registry Endpoint
143
-func (e *Endpoint) String() string {
144
-	return fmt.Sprintf("%s/v%d/", e.URL, e.Version)
145
-}
146
-
147
-// VersionString returns a formatted string of this
148
-// endpoint address using the given API Version.
149
-func (e *Endpoint) VersionString(version APIVersion) string {
150
-	return fmt.Sprintf("%s/v%d/", e.URL, version)
151
-}
152
-
153
-// Path returns a formatted string for the URL
154
-// of this endpoint with the given path appended.
155
-func (e *Endpoint) Path(path string) string {
156
-	return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path)
157
-}
158
-
159
-// Ping pings the remote endpoint with v2 and v1 pings to determine the API
160
-// version. It returns a PingResult containing the discovered version. The
161
-// PingResult also indicates whether the registry is standalone or not.
162
-func (e *Endpoint) Ping() (PingResult, error) {
163
-	// The ping logic to use is determined by the registry endpoint version.
164
-	switch e.Version {
165
-	case APIVersion1:
166
-		return e.pingV1()
167
-	case APIVersion2:
168
-		return e.pingV2()
169
-	}
170
-
171
-	// APIVersionUnknown
172
-	// We should try v2 first...
173
-	e.Version = APIVersion2
174
-	regInfo, errV2 := e.pingV2()
175
-	if errV2 == nil {
176
-		return regInfo, nil
177
-	}
178
-
179
-	// ... then fallback to v1.
180
-	e.Version = APIVersion1
181
-	regInfo, errV1 := e.pingV1()
182
-	if errV1 == nil {
183
-		return regInfo, nil
184
-	}
185
-
186
-	e.Version = APIVersionUnknown
187
-	return PingResult{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1)
188
-}
189
-
190
-func (e *Endpoint) pingV1() (PingResult, error) {
191
-	logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
192
-
193
-	if e.String() == IndexServer {
194
-		// Skip the check, we know this one is valid
195
-		// (and we never want to fallback to http in case of error)
196
-		return PingResult{Standalone: false}, nil
197
-	}
198
-
199
-	req, err := http.NewRequest("GET", e.Path("_ping"), nil)
200
-	if err != nil {
201
-		return PingResult{Standalone: false}, err
202
-	}
203
-
204
-	resp, err := e.client.Do(req)
205
-	if err != nil {
206
-		return PingResult{Standalone: false}, err
207
-	}
208
-
209
-	defer resp.Body.Close()
210
-
211
-	jsonString, err := ioutil.ReadAll(resp.Body)
212
-	if err != nil {
213
-		return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
214
-	}
215
-
216
-	// If the header is absent, we assume true for compatibility with earlier
217
-	// versions of the registry. default to true
218
-	info := PingResult{
219
-		Standalone: true,
220
-	}
221
-	if err := json.Unmarshal(jsonString, &info); err != nil {
222
-		logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
223
-		// don't stop here. Just assume sane defaults
224
-	}
225
-	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
226
-		logrus.Debugf("Registry version header: '%s'", hdr)
227
-		info.Version = hdr
228
-	}
229
-	logrus.Debugf("PingResult.Version: %q", info.Version)
230
-
231
-	standalone := resp.Header.Get("X-Docker-Registry-Standalone")
232
-	logrus.Debugf("Registry standalone header: '%s'", standalone)
233
-	// Accepted values are "true" (case-insensitive) and "1".
234
-	if strings.EqualFold(standalone, "true") || standalone == "1" {
235
-		info.Standalone = true
236
-	} else if len(standalone) > 0 {
237
-		// there is a header set, and it is not "true" or "1", so assume fails
238
-		info.Standalone = false
239
-	}
240
-	logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
241
-	return info, nil
242
-}
243
-
244
-func (e *Endpoint) pingV2() (PingResult, error) {
245
-	logrus.Debugf("attempting v2 ping for registry endpoint %s", e)
246
-
247
-	req, err := http.NewRequest("GET", e.Path(""), nil)
248
-	if err != nil {
249
-		return PingResult{}, err
250
-	}
251
-
252
-	resp, err := e.client.Do(req)
253
-	if err != nil {
254
-		return PingResult{}, err
255
-	}
256
-	defer resp.Body.Close()
257
-
258
-	// The endpoint may have multiple supported versions.
259
-	// Ensure it supports the v2 Registry API.
260
-	var supportsV2 bool
261
-
262
-HeaderLoop:
263
-	for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
264
-		for _, versionName := range strings.Fields(supportedVersions) {
265
-			if versionName == "registry/2.0" {
266
-				supportsV2 = true
267
-				break HeaderLoop
268
-			}
269
-		}
270
-	}
271
-
272
-	if !supportsV2 {
273
-		return PingResult{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e)
274
-	}
275
-
276
-	if resp.StatusCode == http.StatusOK {
277
-		// It would seem that no authentication/authorization is required.
278
-		// So we don't need to parse/add any authorization schemes.
279
-		return PingResult{Standalone: true}, nil
280
-	}
281
-
282
-	if resp.StatusCode == http.StatusUnauthorized {
283
-		// Parse the WWW-Authenticate Header and store the challenges
284
-		// on this endpoint object.
285
-		e.AuthChallenges = parseAuthHeader(resp.Header)
286
-		return PingResult{}, nil
287
-	}
288
-
289
-	return PingResult{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode))
290
-}
... ...
@@ -14,12 +14,13 @@ func TestEndpointParse(t *testing.T) {
14 14
 	}{
15 15
 		{IndexServer, IndexServer},
16 16
 		{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
17
-		{"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"},
18
-		{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"},
19
-		{"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"},
17
+		{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"},
18
+		{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"},
19
+		{"http://0.0.0.0:5000/nonversion/", "http://0.0.0.0:5000/nonversion/v1/"},
20
+		{"http://0.0.0.0:5000/v0/", "http://0.0.0.0:5000/v0/v1/"},
20 21
 	}
21 22
 	for _, td := range testData {
22
-		e, err := newEndpointFromStr(td.str, nil, "", nil)
23
+		e, err := newV1EndpointFromStr(td.str, nil, "", nil)
23 24
 		if err != nil {
24 25
 			t.Errorf("%q: %s", td.str, err)
25 26
 		}
... ...
@@ -33,21 +34,26 @@ func TestEndpointParse(t *testing.T) {
33 33
 	}
34 34
 }
35 35
 
36
+func TestEndpointParseInvalid(t *testing.T) {
37
+	testData := []string{
38
+		"http://0.0.0.0:5000/v2/",
39
+	}
40
+	for _, td := range testData {
41
+		e, err := newV1EndpointFromStr(td, nil, "", nil)
42
+		if err == nil {
43
+			t.Errorf("expected error parsing %q: parsed as %q", td, e)
44
+		}
45
+	}
46
+}
47
+
36 48
 // Ensure that a registry endpoint that responds with a 401 only is determined
37
-// to be a v1 registry unless it includes a valid v2 API header.
38
-func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
49
+// to be a valid v1 registry endpoint
50
+func TestValidateEndpoint(t *testing.T) {
39 51
 	requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40 52
 		w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
41 53
 		w.WriteHeader(http.StatusUnauthorized)
42 54
 	})
43 55
 
44
-	requireBasicAuthHandlerV2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45
-		// This mock server supports v2.0, v2.1, v42.0, and v100.0
46
-		w.Header().Add("Docker-Distribution-API-Version", "registry/100.0 registry/42.0")
47
-		w.Header().Add("Docker-Distribution-API-Version", "registry/2.0 registry/2.1")
48
-		requireBasicAuthHandler.ServeHTTP(w, r)
49
-	})
50
-
51 56
 	// Make a test server which should validate as a v1 server.
52 57
 	testServer := httptest.NewServer(requireBasicAuthHandler)
53 58
 	defer testServer.Close()
... ...
@@ -57,37 +63,16 @@ func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
57 57
 		t.Fatal(err)
58 58
 	}
59 59
 
60
-	testEndpoint := Endpoint{
61
-		URL:     testServerURL,
62
-		Version: APIVersionUnknown,
63
-		client:  HTTPClient(NewTransport(nil)),
60
+	testEndpoint := V1Endpoint{
61
+		URL:    testServerURL,
62
+		client: HTTPClient(NewTransport(nil)),
64 63
 	}
65 64
 
66 65
 	if err = validateEndpoint(&testEndpoint); err != nil {
67 66
 		t.Fatal(err)
68 67
 	}
69 68
 
70
-	if testEndpoint.Version != APIVersion1 {
71
-		t.Fatalf("expected endpoint to validate to %d, got %d", APIVersion1, testEndpoint.Version)
72
-	}
73
-
74
-	// Make a test server which should validate as a v2 server.
75
-	testServer = httptest.NewServer(requireBasicAuthHandlerV2)
76
-	defer testServer.Close()
77
-
78
-	testServerURL, err = url.Parse(testServer.URL)
79
-	if err != nil {
80
-		t.Fatal(err)
81
-	}
82
-
83
-	testEndpoint.URL = testServerURL
84
-	testEndpoint.Version = APIVersionUnknown
85
-
86
-	if err = validateEndpoint(&testEndpoint); err != nil {
87
-		t.Fatal(err)
88
-	}
89
-
90
-	if testEndpoint.Version != APIVersion2 {
91
-		t.Fatalf("expected endpoint to validate to %d, got %d", APIVersion2, testEndpoint.Version)
69
+	if testEndpoint.URL.Scheme != "http" {
70
+		t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
92 71
 	}
93 72
 }
94 73
new file mode 100644
... ...
@@ -0,0 +1,199 @@
0
+package registry
1
+
2
+import (
3
+	"crypto/tls"
4
+	"encoding/json"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"net/url"
9
+	"strings"
10
+
11
+	"github.com/Sirupsen/logrus"
12
+	"github.com/docker/distribution/registry/client/transport"
13
+	registrytypes "github.com/docker/engine-api/types/registry"
14
+)
15
+
16
+// V1Endpoint stores basic information about a V1 registry endpoint.
17
+type V1Endpoint struct {
18
+	client   *http.Client
19
+	URL      *url.URL
20
+	IsSecure bool
21
+}
22
+
23
+// NewV1Endpoint parses the given address to return a registry endpoint.  v can be used to
24
+// specify a specific endpoint version
25
+func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
26
+	tlsConfig, err := newTLSConfig(index.Name, index.Secure)
27
+	if err != nil {
28
+		return nil, err
29
+	}
30
+
31
+	endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders)
32
+	if err != nil {
33
+		return nil, err
34
+	}
35
+
36
+	if err := validateEndpoint(endpoint); err != nil {
37
+		return nil, err
38
+	}
39
+
40
+	return endpoint, nil
41
+}
42
+
43
+func validateEndpoint(endpoint *V1Endpoint) error {
44
+	logrus.Debugf("pinging registry endpoint %s", endpoint)
45
+
46
+	// Try HTTPS ping to registry
47
+	endpoint.URL.Scheme = "https"
48
+	if _, err := endpoint.Ping(); err != nil {
49
+		if endpoint.IsSecure {
50
+			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
51
+			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
52
+			return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
53
+		}
54
+
55
+		// If registry is insecure and HTTPS failed, fallback to HTTP.
56
+		logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
57
+		endpoint.URL.Scheme = "http"
58
+
59
+		var err2 error
60
+		if _, err2 = endpoint.Ping(); err2 == nil {
61
+			return nil
62
+		}
63
+
64
+		return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
65
+	}
66
+
67
+	return nil
68
+}
69
+
70
+func newV1Endpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
71
+	endpoint := &V1Endpoint{
72
+		IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify),
73
+		URL:      new(url.URL),
74
+	}
75
+
76
+	*endpoint.URL = address
77
+
78
+	// TODO(tiborvass): make sure a ConnectTimeout transport is used
79
+	tr := NewTransport(tlsConfig)
80
+	endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(userAgent, metaHeaders)...))
81
+	return endpoint, nil
82
+}
83
+
84
+// trimV1Address trims the version off the address and returns the
85
+// trimmed address or an error if there is a non-V1 version.
86
+func trimV1Address(address string) (string, error) {
87
+	var (
88
+		chunks        []string
89
+		apiVersionStr string
90
+	)
91
+
92
+	if strings.HasSuffix(address, "/") {
93
+		address = address[:len(address)-1]
94
+	}
95
+
96
+	chunks = strings.Split(address, "/")
97
+	apiVersionStr = chunks[len(chunks)-1]
98
+	if apiVersionStr == "v1" {
99
+		return strings.Join(chunks[:len(chunks)-1], "/"), nil
100
+	}
101
+
102
+	for k, v := range apiVersions {
103
+		if k != APIVersion1 && apiVersionStr == v {
104
+			return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr)
105
+		}
106
+	}
107
+
108
+	return address, nil
109
+}
110
+
111
+func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
112
+	if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
113
+		address = "https://" + address
114
+	}
115
+
116
+	address, err := trimV1Address(address)
117
+	if err != nil {
118
+		return nil, err
119
+	}
120
+
121
+	uri, err := url.Parse(address)
122
+	if err != nil {
123
+		return nil, err
124
+	}
125
+
126
+	endpoint, err := newV1Endpoint(*uri, tlsConfig, userAgent, metaHeaders)
127
+	if err != nil {
128
+		return nil, err
129
+	}
130
+
131
+	return endpoint, nil
132
+}
133
+
134
+// Get the formatted URL for the root of this registry Endpoint
135
+func (e *V1Endpoint) String() string {
136
+	return e.URL.String() + "/v1/"
137
+}
138
+
139
+// Path returns a formatted string for the URL
140
+// of this endpoint with the given path appended.
141
+func (e *V1Endpoint) Path(path string) string {
142
+	return e.URL.String() + "/v1/" + path
143
+}
144
+
145
+// Ping returns a PingResult which indicates whether the registry is standalone or not.
146
+func (e *V1Endpoint) Ping() (PingResult, error) {
147
+	logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
148
+
149
+	if e.String() == IndexServer {
150
+		// Skip the check, we know this one is valid
151
+		// (and we never want to fallback to http in case of error)
152
+		return PingResult{Standalone: false}, nil
153
+	}
154
+
155
+	req, err := http.NewRequest("GET", e.Path("_ping"), nil)
156
+	if err != nil {
157
+		return PingResult{Standalone: false}, err
158
+	}
159
+
160
+	resp, err := e.client.Do(req)
161
+	if err != nil {
162
+		return PingResult{Standalone: false}, err
163
+	}
164
+
165
+	defer resp.Body.Close()
166
+
167
+	jsonString, err := ioutil.ReadAll(resp.Body)
168
+	if err != nil {
169
+		return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
170
+	}
171
+
172
+	// If the header is absent, we assume true for compatibility with earlier
173
+	// versions of the registry. default to true
174
+	info := PingResult{
175
+		Standalone: true,
176
+	}
177
+	if err := json.Unmarshal(jsonString, &info); err != nil {
178
+		logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
179
+		// don't stop here. Just assume sane defaults
180
+	}
181
+	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
182
+		logrus.Debugf("Registry version header: '%s'", hdr)
183
+		info.Version = hdr
184
+	}
185
+	logrus.Debugf("PingResult.Version: %q", info.Version)
186
+
187
+	standalone := resp.Header.Get("X-Docker-Registry-Standalone")
188
+	logrus.Debugf("Registry standalone header: '%s'", standalone)
189
+	// Accepted values are "true" (case-insensitive) and "1".
190
+	if strings.EqualFold(standalone, "true") || standalone == "1" {
191
+		info.Standalone = true
192
+	} else if len(standalone) > 0 {
193
+		// there is a header set, and it is not "true" or "1", so assume fails
194
+		info.Standalone = false
195
+	}
196
+	logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
197
+	return info, nil
198
+}
... ...
@@ -25,7 +25,7 @@ const (
25 25
 
26 26
 func spawnTestRegistrySession(t *testing.T) *Session {
27 27
 	authConfig := &types.AuthConfig{}
28
-	endpoint, err := NewEndpoint(makeIndex("/v1/"), "", nil, APIVersionUnknown)
28
+	endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil)
29 29
 	if err != nil {
30 30
 		t.Fatal(err)
31 31
 	}
... ...
@@ -53,7 +53,7 @@ func spawnTestRegistrySession(t *testing.T) *Session {
53 53
 
54 54
 func TestPingRegistryEndpoint(t *testing.T) {
55 55
 	testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) {
56
-		ep, err := NewEndpoint(index, "", nil, APIVersionUnknown)
56
+		ep, err := NewV1Endpoint(index, "", nil)
57 57
 		if err != nil {
58 58
 			t.Fatal(err)
59 59
 		}
... ...
@@ -72,8 +72,8 @@ func TestPingRegistryEndpoint(t *testing.T) {
72 72
 
73 73
 func TestEndpoint(t *testing.T) {
74 74
 	// Simple wrapper to fail test if err != nil
75
-	expandEndpoint := func(index *registrytypes.IndexInfo) *Endpoint {
76
-		endpoint, err := NewEndpoint(index, "", nil, APIVersionUnknown)
75
+	expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint {
76
+		endpoint, err := NewV1Endpoint(index, "", nil)
77 77
 		if err != nil {
78 78
 			t.Fatal(err)
79 79
 		}
... ...
@@ -82,7 +82,7 @@ func TestEndpoint(t *testing.T) {
82 82
 
83 83
 	assertInsecureIndex := func(index *registrytypes.IndexInfo) {
84 84
 		index.Secure = true
85
-		_, err := NewEndpoint(index, "", nil, APIVersionUnknown)
85
+		_, err := NewV1Endpoint(index, "", nil)
86 86
 		assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index")
87 87
 		assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry  error for insecure index")
88 88
 		index.Secure = false
... ...
@@ -90,7 +90,7 @@ func TestEndpoint(t *testing.T) {
90 90
 
91 91
 	assertSecureIndex := func(index *registrytypes.IndexInfo) {
92 92
 		index.Secure = true
93
-		_, err := NewEndpoint(index, "", nil, APIVersionUnknown)
93
+		_, err := NewV1Endpoint(index, "", nil)
94 94
 		assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index")
95 95
 		assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index")
96 96
 		index.Secure = false
... ...
@@ -100,51 +100,33 @@ func TestEndpoint(t *testing.T) {
100 100
 	index.Name = makeURL("/v1/")
101 101
 	endpoint := expandEndpoint(index)
102 102
 	assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
103
-	if endpoint.Version != APIVersion1 {
104
-		t.Fatal("Expected endpoint to be v1")
105
-	}
106 103
 	assertInsecureIndex(index)
107 104
 
108 105
 	index.Name = makeURL("")
109 106
 	endpoint = expandEndpoint(index)
110 107
 	assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
111
-	if endpoint.Version != APIVersion1 {
112
-		t.Fatal("Expected endpoint to be v1")
113
-	}
114 108
 	assertInsecureIndex(index)
115 109
 
116 110
 	httpURL := makeURL("")
117 111
 	index.Name = strings.SplitN(httpURL, "://", 2)[1]
118 112
 	endpoint = expandEndpoint(index)
119 113
 	assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
120
-	if endpoint.Version != APIVersion1 {
121
-		t.Fatal("Expected endpoint to be v1")
122
-	}
123 114
 	assertInsecureIndex(index)
124 115
 
125 116
 	index.Name = makeHTTPSURL("/v1/")
126 117
 	endpoint = expandEndpoint(index)
127 118
 	assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
128
-	if endpoint.Version != APIVersion1 {
129
-		t.Fatal("Expected endpoint to be v1")
130
-	}
131 119
 	assertSecureIndex(index)
132 120
 
133 121
 	index.Name = makeHTTPSURL("")
134 122
 	endpoint = expandEndpoint(index)
135 123
 	assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
136
-	if endpoint.Version != APIVersion1 {
137
-		t.Fatal("Expected endpoint to be v1")
138
-	}
139 124
 	assertSecureIndex(index)
140 125
 
141 126
 	httpsURL := makeHTTPSURL("")
142 127
 	index.Name = strings.SplitN(httpsURL, "://", 2)[1]
143 128
 	endpoint = expandEndpoint(index)
144 129
 	assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
145
-	if endpoint.Version != APIVersion1 {
146
-		t.Fatal("Expected endpoint to be v1")
147
-	}
148 130
 	assertSecureIndex(index)
149 131
 
150 132
 	badEndpoints := []string{
... ...
@@ -156,7 +138,7 @@ func TestEndpoint(t *testing.T) {
156 156
 	}
157 157
 	for _, address := range badEndpoints {
158 158
 		index.Name = address
159
-		_, err := NewEndpoint(index, "", nil, APIVersionUnknown)
159
+		_, err := NewV1Endpoint(index, "", nil)
160 160
 		checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
161 161
 	}
162 162
 }
... ...
@@ -685,7 +667,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
685 685
 	if err != nil {
686 686
 		t.Error(err)
687 687
 	}
688
-	pushAPIEndpoints, err := s.LookupPushEndpoints(imageName)
688
+	pushAPIEndpoints, err := s.LookupPushEndpoints(imageName.Hostname())
689 689
 	if err != nil {
690 690
 		t.Fatal(err)
691 691
 	}
... ...
@@ -693,7 +675,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
693 693
 		t.Fatal("Push endpoint should not contain mirror")
694 694
 	}
695 695
 
696
-	pullAPIEndpoints, err := s.LookupPullEndpoints(imageName)
696
+	pullAPIEndpoints, err := s.LookupPullEndpoints(imageName.Hostname())
697 697
 	if err != nil {
698 698
 		t.Fatal(err)
699 699
 	}
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"net/url"
7 7
 	"strings"
8 8
 
9
+	"github.com/Sirupsen/logrus"
9 10
 	"github.com/docker/docker/reference"
10 11
 	"github.com/docker/engine-api/types"
11 12
 	registrytypes "github.com/docker/engine-api/types/registry"
... ...
@@ -28,29 +29,31 @@ func NewService(options *Options) *Service {
28 28
 // Auth contacts the public registry with the provided credentials,
29 29
 // and returns OK if authentication was successful.
30 30
 // It can be used to verify the validity of a client's credentials.
31
-func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (string, error) {
32
-	addr := authConfig.ServerAddress
33
-	if addr == "" {
34
-		// Use the official registry address if not specified.
35
-		addr = IndexServer
36
-	}
37
-	index, err := s.ResolveIndex(addr)
31
+func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status string, err error) {
32
+	endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress)
38 33
 	if err != nil {
39 34
 		return "", err
40 35
 	}
41 36
 
42
-	endpointVersion := APIVersion(APIVersionUnknown)
43
-	if V2Only {
44
-		// Override the endpoint to only attempt a v2 ping
45
-		endpointVersion = APIVersion2
46
-	}
37
+	for _, endpoint := range endpoints {
38
+		login := loginV2
39
+		if endpoint.Version == APIVersion1 {
40
+			login = loginV1
41
+		}
47 42
 
48
-	endpoint, err := NewEndpoint(index, userAgent, nil, endpointVersion)
49
-	if err != nil {
43
+		status, err = login(authConfig, endpoint, userAgent)
44
+		if err == nil {
45
+			return
46
+		}
47
+		if fErr, ok := err.(fallbackError); ok {
48
+			err = fErr.err
49
+			logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
50
+			continue
51
+		}
50 52
 		return "", err
51 53
 	}
52
-	authConfig.ServerAddress = endpoint.String()
53
-	return Login(authConfig, endpoint)
54
+
55
+	return "", err
54 56
 }
55 57
 
56 58
 // splitReposSearchTerm breaks a search term into an index name and remote name
... ...
@@ -85,7 +88,7 @@ func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent st
85 85
 	}
86 86
 
87 87
 	// *TODO: Search multiple indexes.
88
-	endpoint, err := NewEndpoint(index, userAgent, http.Header(headers), APIVersionUnknown)
88
+	endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers))
89 89
 	if err != nil {
90 90
 		return nil, err
91 91
 	}
... ...
@@ -129,8 +132,8 @@ type APIEndpoint struct {
129 129
 }
130 130
 
131 131
 // ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
132
-func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*Endpoint, error) {
133
-	return newEndpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
132
+func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
133
+	return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
134 134
 }
135 135
 
136 136
 // TLSConfig constructs a client TLS configuration based on server defaults
... ...
@@ -145,15 +148,15 @@ func (s *Service) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
145 145
 // LookupPullEndpoints creates an list of endpoints to try to pull from, in order of preference.
146 146
 // It gives preference to v2 endpoints over v1, mirrors over the actual
147 147
 // registry, and HTTPS over plain HTTP.
148
-func (s *Service) LookupPullEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
149
-	return s.lookupEndpoints(repoName)
148
+func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
149
+	return s.lookupEndpoints(hostname)
150 150
 }
151 151
 
152 152
 // LookupPushEndpoints creates an list of endpoints to try to push to, in order of preference.
153 153
 // It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
154 154
 // Mirrors are not included.
155
-func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
156
-	allEndpoints, err := s.lookupEndpoints(repoName)
155
+func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
156
+	allEndpoints, err := s.lookupEndpoints(hostname)
157 157
 	if err == nil {
158 158
 		for _, endpoint := range allEndpoints {
159 159
 			if !endpoint.Mirror {
... ...
@@ -164,8 +167,8 @@ func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []API
164 164
 	return endpoints, err
165 165
 }
166 166
 
167
-func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
168
-	endpoints, err = s.lookupV2Endpoints(repoName)
167
+func (s *Service) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
168
+	endpoints, err = s.lookupV2Endpoints(hostname)
169 169
 	if err != nil {
170 170
 		return nil, err
171 171
 	}
... ...
@@ -174,7 +177,7 @@ func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndp
174 174
 		return endpoints, nil
175 175
 	}
176 176
 
177
-	legacyEndpoints, err := s.lookupV1Endpoints(repoName)
177
+	legacyEndpoints, err := s.lookupV1Endpoints(hostname)
178 178
 	if err != nil {
179 179
 		return nil, err
180 180
 	}
... ...
@@ -1,19 +1,15 @@
1 1
 package registry
2 2
 
3 3
 import (
4
-	"fmt"
5 4
 	"net/url"
6
-	"strings"
7 5
 
8
-	"github.com/docker/docker/reference"
9 6
 	"github.com/docker/go-connections/tlsconfig"
10 7
 )
11 8
 
12
-func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
9
+func (s *Service) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
13 10
 	var cfg = tlsconfig.ServerDefault
14 11
 	tlsConfig := &cfg
15
-	nameString := repoName.FullName()
16
-	if strings.HasPrefix(nameString, DefaultNamespace+"/") {
12
+	if hostname == DefaultNamespace {
17 13
 		endpoints = append(endpoints, APIEndpoint{
18 14
 			URL:          DefaultV1Registry,
19 15
 			Version:      APIVersion1,
... ...
@@ -24,12 +20,6 @@ func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEn
24 24
 		return endpoints, nil
25 25
 	}
26 26
 
27
-	slashIndex := strings.IndexRune(nameString, '/')
28
-	if slashIndex <= 0 {
29
-		return nil, fmt.Errorf("invalid repo name: missing '/':  %s", nameString)
30
-	}
31
-	hostname := nameString[:slashIndex]
32
-
33 27
 	tlsConfig, err = s.TLSConfig(hostname)
34 28
 	if err != nil {
35 29
 		return nil, err
... ...
@@ -1,19 +1,16 @@
1 1
 package registry
2 2
 
3 3
 import (
4
-	"fmt"
5 4
 	"net/url"
6 5
 	"strings"
7 6
 
8
-	"github.com/docker/docker/reference"
9 7
 	"github.com/docker/go-connections/tlsconfig"
10 8
 )
11 9
 
12
-func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
10
+func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
13 11
 	var cfg = tlsconfig.ServerDefault
14 12
 	tlsConfig := &cfg
15
-	nameString := repoName.FullName()
16
-	if strings.HasPrefix(nameString, DefaultNamespace+"/") {
13
+	if hostname == DefaultNamespace {
17 14
 		// v2 mirrors
18 15
 		for _, mirror := range s.Config.Mirrors {
19 16
 			if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
... ...
@@ -48,12 +45,6 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn
48 48
 		return endpoints, nil
49 49
 	}
50 50
 
51
-	slashIndex := strings.IndexRune(nameString, '/')
52
-	if slashIndex <= 0 {
53
-		return nil, fmt.Errorf("invalid repo name: missing '/':  %s", nameString)
54
-	}
55
-	hostname := nameString[:slashIndex]
56
-
57 51
 	tlsConfig, err = s.TLSConfig(hostname)
58 52
 	if err != nil {
59 53
 		return nil, err
... ...
@@ -37,7 +37,7 @@ var (
37 37
 
38 38
 // A Session is used to communicate with a V1 registry
39 39
 type Session struct {
40
-	indexEndpoint *Endpoint
40
+	indexEndpoint *V1Endpoint
41 41
 	client        *http.Client
42 42
 	// TODO(tiborvass): remove authConfig
43 43
 	authConfig *types.AuthConfig
... ...
@@ -163,7 +163,7 @@ func (tr *authTransport) CancelRequest(req *http.Request) {
163 163
 
164 164
 // NewSession creates a new session
165 165
 // TODO(tiborvass): remove authConfig param once registry client v2 is vendored
166
-func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *Endpoint) (r *Session, err error) {
166
+func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (r *Session, err error) {
167 167
 	r = &Session{
168 168
 		authConfig:    authConfig,
169 169
 		client:        client,
... ...
@@ -175,7 +175,7 @@ func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *End
175 175
 
176 176
 	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
177 177
 	// alongside all our requests.
178
-	if endpoint.VersionString(1) != IndexServer && endpoint.URL.Scheme == "https" {
178
+	if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
179 179
 		info, err := endpoint.Ping()
180 180
 		if err != nil {
181 181
 			return nil, err
... ...
@@ -405,7 +405,7 @@ func buildEndpointsList(headers []string, indexEp string) ([]string, error) {
405 405
 
406 406
 // GetRepositoryData returns lists of images and endpoints for the repository
407 407
 func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) {
408
-	repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), name.RemoteName())
408
+	repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), name.RemoteName())
409 409
 
410 410
 	logrus.Debugf("[registry] Calling GET %s", repositoryTarget)
411 411
 
... ...
@@ -444,7 +444,7 @@ func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, erro
444 444
 
445 445
 	var endpoints []string
446 446
 	if res.Header.Get("X-Docker-Endpoints") != "" {
447
-		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
447
+		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
448 448
 		if err != nil {
449 449
 			return nil, err
450 450
 		}
... ...
@@ -634,7 +634,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
634 634
 	if validate {
635 635
 		suffix = "images"
636 636
 	}
637
-	u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote.RemoteName(), suffix)
637
+	u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote.RemoteName(), suffix)
638 638
 	logrus.Debugf("[registry] PUT %s", u)
639 639
 	logrus.Debugf("Image list pushed to index:\n%s", imgListJSON)
640 640
 	headers := map[string][]string{
... ...
@@ -680,7 +680,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
680 680
 		if res.Header.Get("X-Docker-Endpoints") == "" {
681 681
 			return nil, fmt.Errorf("Index response didn't contain any endpoints")
682 682
 		}
683
-		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
683
+		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
684 684
 		if err != nil {
685 685
 			return nil, err
686 686
 		}
... ...
@@ -722,7 +722,7 @@ func shouldRedirect(response *http.Response) bool {
722 722
 // SearchRepositories performs a search against the remote repository
723 723
 func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) {
724 724
 	logrus.Debugf("Index server: %s", r.indexEndpoint)
725
-	u := r.indexEndpoint.VersionString(1) + "search?q=" + url.QueryEscape(term)
725
+	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term)
726 726
 
727 727
 	req, err := http.NewRequest("GET", u, nil)
728 728
 	if err != nil {
729 729
deleted file mode 100644
... ...
@@ -1,81 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"encoding/json"
5
-	"errors"
6
-	"fmt"
7
-	"net/http"
8
-	"net/url"
9
-	"strings"
10
-)
11
-
12
-type tokenResponse struct {
13
-	Token string `json:"token"`
14
-}
15
-
16
-func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint) (string, error) {
17
-	realm, ok := params["realm"]
18
-	if !ok {
19
-		return "", errors.New("no realm specified for token auth challenge")
20
-	}
21
-
22
-	realmURL, err := url.Parse(realm)
23
-	if err != nil {
24
-		return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
25
-	}
26
-
27
-	if realmURL.Scheme == "" {
28
-		if registryEndpoint.IsSecure {
29
-			realmURL.Scheme = "https"
30
-		} else {
31
-			realmURL.Scheme = "http"
32
-		}
33
-	}
34
-
35
-	req, err := http.NewRequest("GET", realmURL.String(), nil)
36
-	if err != nil {
37
-		return "", err
38
-	}
39
-
40
-	reqParams := req.URL.Query()
41
-	service := params["service"]
42
-	scope := params["scope"]
43
-
44
-	if service != "" {
45
-		reqParams.Add("service", service)
46
-	}
47
-
48
-	for _, scopeField := range strings.Fields(scope) {
49
-		reqParams.Add("scope", scopeField)
50
-	}
51
-
52
-	if username != "" {
53
-		reqParams.Add("account", username)
54
-		req.SetBasicAuth(username, password)
55
-	}
56
-
57
-	req.URL.RawQuery = reqParams.Encode()
58
-
59
-	resp, err := registryEndpoint.client.Do(req)
60
-	if err != nil {
61
-		return "", err
62
-	}
63
-	defer resp.Body.Close()
64
-
65
-	if resp.StatusCode != http.StatusOK {
66
-		return "", fmt.Errorf("token auth attempt for registry %s: %s request failed with status: %d %s", registryEndpoint, req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
67
-	}
68
-
69
-	decoder := json.NewDecoder(resp.Body)
70
-
71
-	tr := new(tokenResponse)
72
-	if err = decoder.Decode(tr); err != nil {
73
-		return "", fmt.Errorf("unable to decode token response: %s", err)
74
-	}
75
-
76
-	if tr.Token == "" {
77
-		return "", errors.New("authorization server did not include a token in the response")
78
-	}
79
-
80
-	return tr.Token, nil
81
-}
... ...
@@ -46,18 +46,18 @@ func (av APIVersion) String() string {
46 46
 	return apiVersions[av]
47 47
 }
48 48
 
49
-var apiVersions = map[APIVersion]string{
50
-	1: "v1",
51
-	2: "v2",
52
-}
53
-
54 49
 // API Version identifiers.
55 50
 const (
56
-	APIVersionUnknown = iota
57
-	APIVersion1
51
+	_                      = iota
52
+	APIVersion1 APIVersion = iota
58 53
 	APIVersion2
59 54
 )
60 55
 
56
+var apiVersions = map[APIVersion]string{
57
+	APIVersion1: "v1",
58
+	APIVersion2: "v2",
59
+}
60
+
61 61
 // RepositoryInfo describes a repository
62 62
 type RepositoryInfo struct {
63 63
 	reference.Named