Skip to content

Commit 35b00eb

Browse files
committed
docker: mimic docker upstream registry authentication
Signed-off-by: Antonio Murdaca <[email protected]>
1 parent fbc7955 commit 35b00eb

File tree

3 files changed

+51
-101
lines changed

3 files changed

+51
-101
lines changed

docker/docker_client.go

Lines changed: 48 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,20 @@ var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
4545

4646
// dockerClient is configuration for dealing with a single Docker registry.
4747
type dockerClient struct {
48-
ctx *types.SystemContext
49-
registry string
50-
username string
51-
password string
52-
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
53-
scheme string // Cache of a value returned by a successful ping() if not empty
54-
client *http.Client
55-
signatureBase signatureStorageBase
48+
ctx *types.SystemContext
49+
registry string
50+
username string
51+
password string
52+
scheme string // Cache of a value returned by a successful ping() if not empty
53+
client *http.Client
54+
signatureBase signatureStorageBase
55+
challenges []challenge
56+
scope authScope
57+
}
58+
59+
type authScope struct {
60+
remoteName string
61+
actions string
5662
}
5763

5864
// this is cloned from docker/go-connections because upstream docker has changed
@@ -147,7 +153,7 @@ func hasFile(files []os.FileInfo, name string) bool {
147153

148154
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
149155
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
150-
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool) (*dockerClient, error) {
156+
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
151157
registry := ref.ref.Hostname()
152158
if registry == dockerHostname {
153159
registry = dockerRegistry
@@ -184,19 +190,20 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
184190
password: password,
185191
client: client,
186192
signatureBase: sigBase,
193+
scope: authScope{
194+
actions: actions,
195+
remoteName: ref.ref.RemoteName(),
196+
},
187197
}, nil
188198
}
189199

190200
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
191201
// url is NOT an absolute URL, but a path relative to the /v2/ top-level API path. The host name and schema is taken from the client or autodetected.
192202
func (c *dockerClient) makeRequest(method, url string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
193203
if c.scheme == "" {
194-
pr, err := c.ping()
195-
if err != nil {
204+
if err := c.ping(); err != nil {
196205
return nil, err
197206
}
198-
c.wwwAuthenticate = pr.WWWAuthenticate
199-
c.scheme = pr.scheme
200207
}
201208

202209
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
@@ -224,7 +231,7 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
224231
if c.ctx != nil && c.ctx.DockerRegistryUserAgent != "" {
225232
req.Header.Add("User-Agent", c.ctx.DockerRegistryUserAgent)
226233
}
227-
if c.wwwAuthenticate != "" && sendAuth {
234+
if sendAuth {
228235
if err := c.setupRequestAuth(req); err != nil {
229236
return nil, err
230237
}
@@ -237,87 +244,38 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237244
return res, nil
238245
}
239246

247+
// we're using the challenges from the /v2/ ping response and not the one from the destination
248+
// URL in this request because:
249+
//
250+
// 1) docker does that as well
251+
// 2) gcr.io is sending 401 without a WWW-Authenticate header in the real request
252+
//
253+
// debugging: https://github.com/containers/image/pull/211#issuecomment-273426236 and follows up
240254
func (c *dockerClient) setupRequestAuth(req *http.Request) error {
241-
tokens := strings.SplitN(strings.TrimSpace(c.wwwAuthenticate), " ", 2)
242-
if len(tokens) != 2 {
243-
return errors.Errorf("expected 2 tokens in WWW-Authenticate: %d, %s", len(tokens), c.wwwAuthenticate)
255+
if len(c.challenges) == 0 {
256+
return nil
244257
}
245-
switch tokens[0] {
246-
case "Basic":
258+
// assume just one...
259+
challenge := c.challenges[0]
260+
switch challenge.Scheme {
261+
case "basic":
247262
req.SetBasicAuth(c.username, c.password)
248263
return nil
249-
case "Bearer":
250-
// FIXME? This gets a new token for every API request;
251-
// we may be easily able to reuse a previous token, e.g.
252-
// for OpenShift the token only identifies the user and does not vary
253-
// across operations. Should we just try the request first, and
254-
// only get a new token on failure?
255-
// OTOH what to do with the single-use body stream in that case?
256-
257-
// Try performing the request, expecting it to fail.
258-
testReq := *req
259-
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
260-
testReq.Body = nil
261-
testReq.ContentLength = 0
262-
res, err := c.client.Do(&testReq)
263-
if err != nil {
264-
return err
265-
}
266-
chs := parseAuthHeader(res.Header)
267-
// We could end up in this "if" statement if the /v2/ call (during ping)
268-
// returned 401 with a valid WWW-Authenticate=Bearer header.
269-
// That doesn't **always** mean, however, that the specific API request
270-
// (different from /v2/) actually needs to be authorized.
271-
// One example of this _weird_ scenario happens with GCR.io docker
272-
// registries.
273-
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
274-
// With gcr.io, the /v2/ call returns a 401 with a valid WWW-Authenticate=Bearer
275-
// header but the repository could be _public_ (no authorization is needed).
276-
// Hence, the registry response contains no challenges and the status
277-
// code is not 401.
278-
// We just skip this case as it's not standard on docker/distribution
279-
// registries (https://github.com/docker/distribution/blob/master/docs/spec/api.md#api-version-check)
280-
if res.StatusCode != http.StatusUnauthorized {
281-
return nil
282-
}
283-
// gcr.io private repositories pull instead requires us to send user:pass pair in
284-
// order to retrieve a token and setup the correct Bearer token.
285-
// try again one last time with Basic Auth
286-
testReq2 := *req
287-
// Do not use the body stream, or we couldn't reuse it for the "real" call later.
288-
testReq2.Body = nil
289-
testReq2.ContentLength = 0
290-
testReq2.SetBasicAuth(c.username, c.password)
291-
res, err := c.client.Do(&testReq2)
292-
if err != nil {
293-
return err
294-
}
295-
chs = parseAuthHeader(res.Header)
296-
if res.StatusCode != http.StatusUnauthorized || chs == nil || len(chs) == 0 {
297-
// no need for bearer? wtf?
298-
return nil
299-
}
300-
}
301-
// Arbitrarily use the first challenge, there is no reason to expect more than one.
302-
challenge := chs[0]
303-
if challenge.Scheme != "bearer" { // Another artifact of trying to handle WWW-Authenticate before it actually happens.
304-
return errors.Errorf("Unimplemented: WWW-Authenticate Bearer replaced by %#v", challenge.Scheme)
305-
}
264+
case "bearer":
306265
realm, ok := challenge.Parameters["realm"]
307266
if !ok {
308267
return errors.Errorf("missing realm in bearer auth challenge")
309268
}
310269
service, _ := challenge.Parameters["service"] // Will be "" if not present
311-
scope, _ := challenge.Parameters["scope"] // Will be "" if not present
270+
scope := fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions)
312271
token, err := c.getBearerToken(realm, service, scope)
313272
if err != nil {
314273
return err
315274
}
316275
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
317276
return nil
318277
}
319-
return errors.Errorf("no handler for %s authentication", tokens[0])
320-
// support docker bearer with authconfig's Auth string? see docker2aci
278+
return errors.Errorf("no handler for %s authentication", challenge.Scheme)
321279
}
322280

323281
func (c *dockerClient) getBearerToken(realm, service, scope string) (string, error) {
@@ -427,39 +385,31 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
427385
return "", "", nil
428386
}
429387

430-
type pingResponse struct {
431-
WWWAuthenticate string
432-
APIVersion string
433-
scheme string
434-
}
435-
436-
func (c *dockerClient) ping() (*pingResponse, error) {
437-
ping := func(scheme string) (*pingResponse, error) {
388+
func (c *dockerClient) ping() error {
389+
ping := func(scheme string) error {
438390
url := fmt.Sprintf(baseURL, scheme, c.registry)
439391
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
440392
logrus.Debugf("Ping %s err %#v", url, err)
441393
if err != nil {
442-
return nil, err
394+
return err
443395
}
444396
defer resp.Body.Close()
445397
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v2/", resp.StatusCode)
446398
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
447-
return nil, errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
399+
return errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
448400
}
449-
pr := &pingResponse{}
450-
pr.WWWAuthenticate = resp.Header.Get("WWW-Authenticate")
451-
pr.APIVersion = resp.Header.Get("Docker-Distribution-Api-Version")
452-
pr.scheme = scheme
453-
return pr, nil
401+
c.challenges = parseAuthHeader(resp.Header)
402+
c.scheme = scheme
403+
return nil
454404
}
455-
pr, err := ping("https")
405+
err := ping("https")
456406
if err != nil && c.ctx != nil && c.ctx.DockerInsecureSkipTLSVerify {
457-
pr, err = ping("http")
407+
err = ping("http")
458408
}
459409
if err != nil {
460410
err = errors.Wrap(err, "pinging docker registry returned")
461411
if c.ctx != nil && c.ctx.DockerDisableV1Ping {
462-
return nil, err
412+
return err
463413
}
464414
// best effort to understand if we're talking to a V1 registry
465415
pingV1 := func(scheme string) bool {
@@ -484,7 +434,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
484434
err = ErrV1NotSupported
485435
}
486436
}
487-
return pr, err
437+
return err
488438
}
489439

490440
func getDefaultConfigDir(confPath string) string {

docker/docker_image_dest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ type dockerImageDestination struct {
4141

4242
// newImageDestination creates a new ImageDestination for the specified image reference.
4343
func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) {
44-
c, err := newDockerClient(ctx, ref, true)
44+
c, err := newDockerClient(ctx, ref, true, "push")
4545
if err != nil {
4646
return nil, err
4747
}

docker/docker_image_src.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type dockerImageSource struct {
3232
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
3333
// The caller must call .Close() on the returned ImageSource.
3434
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) {
35-
c, err := newDockerClient(ctx, ref, false)
35+
c, err := newDockerClient(ctx, ref, false, "pull")
3636
if err != nil {
3737
return nil, err
3838
}
@@ -261,7 +261,7 @@ func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, mis
261261

262262
// deleteImage deletes the named image from the registry, if supported.
263263
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
264-
c, err := newDockerClient(ctx, ref, true)
264+
c, err := newDockerClient(ctx, ref, true, "push")
265265
if err != nil {
266266
return err
267267
}

0 commit comments

Comments
 (0)