Browse code

Login update and endpoint refactor

Further differentiate the APIEndpoint used with V2 with the endpoint type which is only used for v1 registry interactions
Rename Endpoint to V1Endpoint and remove version ambiguity
Use distribution token handler for login

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Derek McGowan authored on 2016/03/01 16:07:41
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