@@ -45,14 +45,20 @@ var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
45
45
46
46
// dockerClient is configuration for dealing with a single Docker registry.
47
47
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
56
62
}
57
63
58
64
// this is cloned from docker/go-connections because upstream docker has changed
@@ -147,7 +153,7 @@ func hasFile(files []os.FileInfo, name string) bool {
147
153
148
154
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
149
155
// “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 ) {
151
157
registry := ref .ref .Hostname ()
152
158
if registry == dockerHostname {
153
159
registry = dockerRegistry
@@ -184,19 +190,20 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
184
190
password : password ,
185
191
client : client ,
186
192
signatureBase : sigBase ,
193
+ scope : authScope {
194
+ actions : actions ,
195
+ remoteName : ref .ref .RemoteName (),
196
+ },
187
197
}, nil
188
198
}
189
199
190
200
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
191
201
// 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.
192
202
func (c * dockerClient ) makeRequest (method , url string , headers map [string ][]string , stream io.Reader ) (* http.Response , error ) {
193
203
if c .scheme == "" {
194
- pr , err := c .ping ()
195
- if err != nil {
204
+ if err := c .ping (); err != nil {
196
205
return nil , err
197
206
}
198
- c .wwwAuthenticate = pr .WWWAuthenticate
199
- c .scheme = pr .scheme
200
207
}
201
208
202
209
url = fmt .Sprintf (baseURL , c .scheme , c .registry ) + url
@@ -224,7 +231,7 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
224
231
if c .ctx != nil && c .ctx .DockerRegistryUserAgent != "" {
225
232
req .Header .Add ("User-Agent" , c .ctx .DockerRegistryUserAgent )
226
233
}
227
- if c . wwwAuthenticate != "" && sendAuth {
234
+ if sendAuth {
228
235
if err := c .setupRequestAuth (req ); err != nil {
229
236
return nil , err
230
237
}
@@ -237,87 +244,38 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237
244
return res , nil
238
245
}
239
246
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
240
254
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
244
257
}
245
- switch tokens [0 ] {
246
- case "Basic" :
258
+ // assume just one...
259
+ challenge := c .challenges [0 ]
260
+ switch challenge .Scheme {
261
+ case "basic" :
247
262
req .SetBasicAuth (c .username , c .password )
248
263
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" :
306
265
realm , ok := challenge .Parameters ["realm" ]
307
266
if ! ok {
308
267
return errors .Errorf ("missing realm in bearer auth challenge" )
309
268
}
310
269
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 )
312
271
token , err := c .getBearerToken (realm , service , scope )
313
272
if err != nil {
314
273
return err
315
274
}
316
275
req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
317
276
return nil
318
277
}
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 )
321
279
}
322
280
323
281
func (c * dockerClient ) getBearerToken (realm , service , scope string ) (string , error ) {
@@ -427,39 +385,31 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
427
385
return "" , "" , nil
428
386
}
429
387
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 {
438
390
url := fmt .Sprintf (baseURL , scheme , c .registry )
439
391
resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true )
440
392
logrus .Debugf ("Ping %s err %#v" , url , err )
441
393
if err != nil {
442
- return nil , err
394
+ return err
443
395
}
444
396
defer resp .Body .Close ()
445
397
logrus .Debugf ("Ping %s status %d" , scheme + "://" + c .registry + "/v2/" , resp .StatusCode )
446
398
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 )
448
400
}
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
454
404
}
455
- pr , err := ping ("https" )
405
+ err := ping ("https" )
456
406
if err != nil && c .ctx != nil && c .ctx .DockerInsecureSkipTLSVerify {
457
- pr , err = ping ("http" )
407
+ err = ping ("http" )
458
408
}
459
409
if err != nil {
460
410
err = errors .Wrap (err , "pinging docker registry returned" )
461
411
if c .ctx != nil && c .ctx .DockerDisableV1Ping {
462
- return nil , err
412
+ return err
463
413
}
464
414
// best effort to understand if we're talking to a V1 registry
465
415
pingV1 := func (scheme string ) bool {
@@ -484,7 +434,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
484
434
err = ErrV1NotSupported
485
435
}
486
436
}
487
- return pr , err
437
+ return err
488
438
}
489
439
490
440
func getDefaultConfigDir (confPath string ) string {
0 commit comments