Update login to use token handling code from distribution
| ... | ... |
@@ -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 |