9
9
"io/ioutil"
10
10
"net"
11
11
"net/http"
12
+ "net/url"
12
13
"os"
13
14
"path/filepath"
14
15
"strings"
@@ -45,14 +46,14 @@ var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
45
46
46
47
// dockerClient is configuration for dealing with a single Docker registry.
47
48
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
49
+ ctx * types.SystemContext
50
+ registry string
51
+ username string
52
+ password string
53
+ scheme string // Cache of a value returned by a successful ping() if not empty
54
+ client * http. Client
55
+ signatureBase signatureStorageBase
56
+ challenges map [ string ][] challenge
56
57
}
57
58
58
59
// this is cloned from docker/go-connections because upstream docker has changed
@@ -184,30 +185,35 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
184
185
password : password ,
185
186
client : client ,
186
187
signatureBase : sigBase ,
188
+ challenges : make (map [string ][]challenge ),
187
189
}, nil
188
190
}
189
191
192
+ type requestOptions struct {
193
+ remoteName string
194
+ actions string
195
+ }
196
+
190
197
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
191
198
// 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 ) {
199
+ func (c * dockerClient ) makeRequest (method , url string , headers map [string ][]string , stream io.Reader , opts requestOptions ) (* http.Response , error ) {
193
200
if c .scheme == "" {
194
201
pr , err := c .ping ()
195
202
if err != nil {
196
203
return nil , err
197
204
}
198
- c .wwwAuthenticate = pr .WWWAuthenticate
199
205
c .scheme = pr .scheme
200
206
}
201
207
202
208
url = fmt .Sprintf (baseURL , c .scheme , c .registry ) + url
203
- return c .makeRequestToResolvedURL (method , url , headers , stream , - 1 , true )
209
+ return c .makeRequestToResolvedURL (method , url , headers , stream , - 1 , true , opts )
204
210
}
205
211
206
212
// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
207
213
// streamLen, if not -1, specifies the length of the data expected on stream.
208
214
// makeRequest should generally be preferred.
209
215
// 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 ) {
216
+ func (c * dockerClient ) makeRequestToResolvedURL (method , url string , headers map [string ][]string , stream io.Reader , streamLen int64 , sendAuth bool , opts requestOptions ) (* http.Response , error ) {
211
217
req , err := http .NewRequest (method , url , stream )
212
218
if err != nil {
213
219
return nil , err
@@ -224,8 +230,8 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
224
230
if c .ctx != nil && c .ctx .DockerRegistryUserAgent != "" {
225
231
req .Header .Add ("User-Agent" , c .ctx .DockerRegistryUserAgent )
226
232
}
227
- if c . wwwAuthenticate != "" && sendAuth {
228
- if err := c .setupRequestAuth (req ); err != nil {
233
+ if sendAuth {
234
+ if err := c .setupRequestAuth (req , opts . remoteName , opts . actions ); err != nil {
229
235
return nil , err
230
236
}
231
237
}
@@ -237,87 +243,32 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237
243
return res , nil
238
244
}
239
245
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 )
246
+ func (c * dockerClient ) setupRequestAuth (req * http.Request , remoteName , actions string ) error {
247
+ chs := c . getChallenges ( req )
248
+ if chs == nil || len (chs ) == 0 {
249
+ return nil
244
250
}
245
- switch tokens [0 ] {
246
- case "Basic" :
251
+ // assume just one...
252
+ challenge := chs [0 ]
253
+ switch challenge .Scheme {
254
+ case "basic" :
247
255
req .SetBasicAuth (c .username , c .password )
248
256
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
- }
257
+ case "bearer" :
306
258
realm , ok := challenge .Parameters ["realm" ]
307
259
if ! ok {
308
260
return errors .Errorf ("missing realm in bearer auth challenge" )
309
261
}
310
262
service , _ := challenge .Parameters ["service" ] // Will be "" if not present
311
- scope , _ := challenge . Parameters [ "scope" ] // Will be "" if not present
263
+ scope := fmt . Sprintf ( "repository:%s:%s" , remoteName , actions )
312
264
token , err := c .getBearerToken (realm , service , scope )
313
265
if err != nil {
314
266
return err
315
267
}
316
268
req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
317
269
return nil
318
270
}
319
- return errors .Errorf ("no handler for %s authentication" , tokens [0 ])
320
- // support docker bearer with authconfig's Auth string? see docker2aci
271
+ return errors .Errorf ("no handler for %s authentication" , challenge .Scheme )
321
272
}
322
273
323
274
func (c * dockerClient ) getBearerToken (realm , service , scope string ) (string , error ) {
@@ -428,15 +379,34 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
428
379
}
429
380
430
381
type pingResponse struct {
431
- WWWAuthenticate string
432
- APIVersion string
433
- scheme string
382
+ APIVersion string
383
+ scheme string
384
+ }
385
+
386
+ func (c * dockerClient ) saveChallenges (chs []challenge , u * url.URL ) {
387
+ urlCopy := url.URL {
388
+ Path : u .Path ,
389
+ Host : u .Host ,
390
+ Scheme : u .Scheme ,
391
+ }
392
+ normalizeURL (& urlCopy )
393
+ c .challenges [urlCopy .String ()] = chs
394
+ }
395
+
396
+ func (c * dockerClient ) getChallenges (req * http.Request ) []challenge {
397
+ ping := url.URL {
398
+ Host : req .URL .Host ,
399
+ Scheme : req .URL .Scheme ,
400
+ Path : "/v2/" ,
401
+ }
402
+ normalizeURL (& ping )
403
+ return c .challenges [ping .String ()]
434
404
}
435
405
436
406
func (c * dockerClient ) ping () (* pingResponse , error ) {
437
407
ping := func (scheme string ) (* pingResponse , error ) {
438
408
url := fmt .Sprintf (baseURL , scheme , c .registry )
439
- resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true )
409
+ resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true , requestOptions {} )
440
410
logrus .Debugf ("Ping %s err %#v" , url , err )
441
411
if err != nil {
442
412
return nil , err
@@ -446,8 +416,10 @@ func (c *dockerClient) ping() (*pingResponse, error) {
446
416
if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusUnauthorized {
447
417
return nil , errors .Errorf ("error pinging repository, response code %d" , resp .StatusCode )
448
418
}
419
+ chs := parseAuthHeader (resp .Header )
420
+ c .saveChallenges (chs , resp .Request .URL )
421
+
449
422
pr := & pingResponse {}
450
- pr .WWWAuthenticate = resp .Header .Get ("WWW-Authenticate" )
451
423
pr .APIVersion = resp .Header .Get ("Docker-Distribution-Api-Version" )
452
424
pr .scheme = scheme
453
425
return pr , nil
@@ -464,7 +436,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
464
436
// best effort to understand if we're talking to a V1 registry
465
437
pingV1 := func (scheme string ) bool {
466
438
url := fmt .Sprintf (baseURLV1 , scheme , c .registry )
467
- resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true )
439
+ resp , err := c .makeRequestToResolvedURL ("GET" , url , nil , nil , - 1 , true , requestOptions {} )
468
440
logrus .Debugf ("Ping %s err %#v" , url , err )
469
441
if err != nil {
470
442
return false
0 commit comments