@@ -45,14 +45,14 @@ 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
56
}
57
57
58
58
// this is cloned from docker/go-connections because upstream docker has changed
@@ -187,45 +187,55 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
187
187
}, nil
188
188
}
189
189
190
+ type authScope struct {
191
+ remoteName string
192
+ actions string
193
+ }
194
+
195
+ type requestOptions struct {
196
+ scope authScope
197
+ headers map [string ][]string
198
+ stream io.Reader
199
+ }
200
+
190
201
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
191
202
// 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
- func (c * dockerClient ) makeRequest (method , url string , headers map [ string ][] string , stream io. Reader ) (* http.Response , error ) {
203
+ func (c * dockerClient ) makeRequest (method , url string , opts requestOptions ) (* http.Response , error ) {
193
204
if c .scheme == "" {
194
205
pr , err := c .ping ()
195
206
if err != nil {
196
207
return nil , err
197
208
}
198
- c .wwwAuthenticate = pr .WWWAuthenticate
199
209
c .scheme = pr .scheme
200
210
}
201
211
202
212
url = fmt .Sprintf (baseURL , c .scheme , c .registry ) + url
203
- return c .makeRequestToResolvedURL (method , url , headers , stream , - 1 , true )
213
+ return c .makeRequestToResolvedURL (method , url , opts , - 1 , true )
204
214
}
205
215
206
216
// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
207
217
// streamLen, if not -1, specifies the length of the data expected on stream.
208
218
// makeRequest should generally be preferred.
209
219
// TODO(runcom): too many arguments here, use a struct
210
- func (c * dockerClient ) makeRequestToResolvedURL (method , url string , headers map [ string ][] string , stream io. Reader , streamLen int64 , sendAuth bool ) (* http.Response , error ) {
211
- req , err := http .NewRequest (method , url , stream )
220
+ func (c * dockerClient ) makeRequestToResolvedURL (method , url string , opts requestOptions , streamLen int64 , sendAuth bool ) (* http.Response , error ) {
221
+ req , err := http .NewRequest (method , url , opts . stream )
212
222
if err != nil {
213
223
return nil , err
214
224
}
215
225
if streamLen != - 1 { // Do not blindly overwrite if streamLen == -1, http.NewRequest above can figure out the length of bytes.Reader and similar objects without us having to compute it.
216
226
req .ContentLength = streamLen
217
227
}
218
228
req .Header .Set ("Docker-Distribution-API-Version" , "registry/2.0" )
219
- for n , h := range headers {
229
+ for n , h := range opts . headers {
220
230
for _ , hh := range h {
221
231
req .Header .Add (n , hh )
222
232
}
223
233
}
224
234
if c .ctx != nil && c .ctx .DockerRegistryUserAgent != "" {
225
235
req .Header .Add ("User-Agent" , c .ctx .DockerRegistryUserAgent )
226
236
}
227
- if c . wwwAuthenticate != "" && sendAuth {
228
- if err := c .setupRequestAuth (req ); err != nil {
237
+ if sendAuth {
238
+ if err := c .setupRequestAuth (req , opts . scope . remoteName , opts . scope . actions ); err != nil {
229
239
return nil , err
230
240
}
231
241
}
@@ -237,87 +247,31 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237
247
return res , nil
238
248
}
239
249
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 )
250
+ func (c * dockerClient ) setupRequestAuth (req * http.Request , remoteName , actions string ) error {
251
+ if len (c .challenges ) == 0 {
252
+ return nil
244
253
}
245
- switch tokens [0 ] {
246
- case "Basic" :
254
+ // assume just one...
255
+ challenge := c .challenges [0 ]
256
+ switch challenge .Scheme {
257
+ case "basic" :
247
258
req .SetBasicAuth (c .username , c .password )
248
259
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
- }
260
+ case "bearer" :
306
261
realm , ok := challenge .Parameters ["realm" ]
307
262
if ! ok {
308
263
return errors .Errorf ("missing realm in bearer auth challenge" )
309
264
}
310
265
service , _ := challenge .Parameters ["service" ] // Will be "" if not present
311
- scope , _ := challenge . Parameters [ "scope" ] // Will be "" if not present
266
+ scope := fmt . Sprintf ( "repository:%s:%s" , remoteName , actions )
312
267
token , err := c .getBearerToken (realm , service , scope )
313
268
if err != nil {
314
269
return err
315
270
}
316
271
req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
317
272
return nil
318
273
}
319
- return errors .Errorf ("no handler for %s authentication" , tokens [0 ])
320
- // support docker bearer with authconfig's Auth string? see docker2aci
274
+ return errors .Errorf ("no handler for %s authentication" , challenge .Scheme )
321
275
}
322
276
323
277
func (c * dockerClient ) getBearerToken (realm , service , scope string ) (string , error ) {
@@ -428,15 +382,14 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
428
382
}
429
383
430
384
type pingResponse struct {
431
- WWWAuthenticate string
432
- APIVersion string
433
- scheme string
385
+ APIVersion string
386
+ scheme string
434
387
}
435
388
436
389
func (c * dockerClient ) ping () (* pingResponse , error ) {
437
390
ping := func (scheme string ) (* pingResponse , error ) {
438
391
url := fmt .Sprintf (baseURL , scheme , c .registry )
439
- resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true )
392
+ resp , err := c .makeRequestToResolvedURL ("GET" , url , requestOptions {} , - 1 , true )
440
393
logrus .Debugf ("Ping %s err %#v" , url , err )
441
394
if err != nil {
442
395
return nil , err
@@ -446,8 +399,9 @@ func (c *dockerClient) ping() (*pingResponse, error) {
446
399
if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusUnauthorized {
447
400
return nil , errors .Errorf ("error pinging repository, response code %d" , resp .StatusCode )
448
401
}
402
+ c .challenges = parseAuthHeader (resp .Header )
403
+
449
404
pr := & pingResponse {}
450
- pr .WWWAuthenticate = resp .Header .Get ("WWW-Authenticate" )
451
405
pr .APIVersion = resp .Header .Get ("Docker-Distribution-Api-Version" )
452
406
pr .scheme = scheme
453
407
return pr , nil
@@ -464,7 +418,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
464
418
// best effort to understand if we're talking to a V1 registry
465
419
pingV1 := func (scheme string ) bool {
466
420
url := fmt .Sprintf (baseURLV1 , scheme , c .registry )
467
- resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true )
421
+ resp , err := c .makeRequestToResolvedURL ("GET" , url , requestOptions {} , - 1 , true )
468
422
logrus .Debugf ("Ping %s err %#v" , url , err )
469
423
if err != nil {
470
424
return false
0 commit comments