@@ -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,6 +190,10 @@ 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
@@ -195,8 +205,6 @@ func (c *dockerClient) makeRequest(method, url string, headers map[string][]stri
195
205
if err != nil {
196
206
return nil , err
197
207
}
198
- c .wwwAuthenticate = pr .WWWAuthenticate
199
- c .scheme = pr .scheme
200
208
}
201
209
202
210
url = fmt .Sprintf (baseURL , c .scheme , c .registry ) + url
@@ -224,8 +232,8 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
224
232
if c .ctx != nil && c .ctx .DockerRegistryUserAgent != "" {
225
233
req .Header .Add ("User-Agent" , c .ctx .DockerRegistryUserAgent )
226
234
}
227
- if c . wwwAuthenticate != "" && sendAuth {
228
- if err := c .setupRequestAuth (req ); err != nil {
235
+ if sendAuth {
236
+ if err := c .setupRequestAuth (req , c . scope . remoteName , c . scope . actions ); err != nil {
229
237
return nil , err
230
238
}
231
239
}
@@ -237,87 +245,31 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237
245
return res , nil
238
246
}
239
247
240
- 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 )
248
+ func (c * dockerClient ) setupRequestAuth (req * http.Request , remoteName , actions string ) error {
249
+ if len (c .challenges ) == 0 {
250
+ return nil
244
251
}
245
- switch tokens [0 ] {
246
- case "Basic" :
252
+ // assume just one...
253
+ challenge := c .challenges [0 ]
254
+ switch challenge .Scheme {
255
+ case "basic" :
247
256
req .SetBasicAuth (c .username , c .password )
248
257
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
- }
258
+ case "bearer" :
306
259
realm , ok := challenge .Parameters ["realm" ]
307
260
if ! ok {
308
261
return errors .Errorf ("missing realm in bearer auth challenge" )
309
262
}
310
263
service , _ := challenge .Parameters ["service" ] // Will be "" if not present
311
- scope , _ := challenge . Parameters [ "scope" ] // Will be "" if not present
264
+ scope := fmt . Sprintf ( "repository:%s:%s" , remoteName , actions )
312
265
token , err := c .getBearerToken (realm , service , scope )
313
266
if err != nil {
314
267
return err
315
268
}
316
269
req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
317
270
return nil
318
271
}
319
- return errors .Errorf ("no handler for %s authentication" , tokens [0 ])
320
- // support docker bearer with authconfig's Auth string? see docker2aci
272
+ return errors .Errorf ("no handler for %s authentication" , challenge .Scheme )
321
273
}
322
274
323
275
func (c * dockerClient ) getBearerToken (realm , service , scope string ) (string , error ) {
@@ -427,12 +379,6 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
427
379
return "" , "" , nil
428
380
}
429
381
430
- type pingResponse struct {
431
- WWWAuthenticate string
432
- APIVersion string
433
- scheme string
434
- }
435
-
436
382
func (c * dockerClient ) ping () (* pingResponse , error ) {
437
383
ping := func (scheme string ) (* pingResponse , error ) {
438
384
url := fmt .Sprintf (baseURL , scheme , c .registry )
@@ -446,10 +392,8 @@ func (c *dockerClient) ping() (*pingResponse, error) {
446
392
if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusUnauthorized {
447
393
return nil , errors .Errorf ("error pinging repository, response code %d" , resp .StatusCode )
448
394
}
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
395
+ c .challenges = parseAuthHeader (resp .Header )
396
+ c .scheme = scheme
453
397
return pr , nil
454
398
}
455
399
pr , err := ping ("https" )
@@ -464,7 +408,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
464
408
// best effort to understand if we're talking to a V1 registry
465
409
pingV1 := func (scheme string ) bool {
466
410
url := fmt .Sprintf (baseURLV1 , scheme , c .registry )
467
- resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true )
411
+ resp , err := c .makeRequestToResolvedURL ("GET" , url , requestOptions {} , - 1 , true )
468
412
logrus .Debugf ("Ping %s err %#v" , url , err )
469
413
if err != nil {
470
414
return false
0 commit comments