99 "io/ioutil"
1010 "net"
1111 "net/http"
12+ "net/url"
1213 "os"
1314 "path/filepath"
1415 "strings"
@@ -45,14 +46,14 @@ var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
4546
4647// dockerClient is configuration for dealing with a single Docker registry.
4748type 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
5657}
5758
5859// 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)
184185 password : password ,
185186 client : client ,
186187 signatureBase : sigBase ,
188+ challenges : make (map [string ][]challenge ),
187189 }, nil
188190}
189191
192+ type requestOptions struct {
193+ remoteName string
194+ actions string
195+ }
196+
190197// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
191198// 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 ) {
193200 if c .scheme == "" {
194201 pr , err := c .ping ()
195202 if err != nil {
196203 return nil , err
197204 }
198- c .wwwAuthenticate = pr .WWWAuthenticate
199205 c .scheme = pr .scheme
200206 }
201207
202208 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 )
204210}
205211
206212// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
207213// streamLen, if not -1, specifies the length of the data expected on stream.
208214// makeRequest should generally be preferred.
209215// 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 ) {
211217 req , err := http .NewRequest (method , url , stream )
212218 if err != nil {
213219 return nil , err
@@ -224,8 +230,8 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
224230 if c .ctx != nil && c .ctx .DockerRegistryUserAgent != "" {
225231 req .Header .Add ("User-Agent" , c .ctx .DockerRegistryUserAgent )
226232 }
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 {
229235 return nil , err
230236 }
231237 }
@@ -237,87 +243,32 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237243 return res , nil
238244}
239245
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
244250 }
245- switch tokens [0 ] {
246- case "Basic" :
251+ // assume just one...
252+ challenge := chs [0 ]
253+ switch challenge .Scheme {
254+ case "basic" :
247255 req .SetBasicAuth (c .username , c .password )
248256 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" :
306258 realm , ok := challenge .Parameters ["realm" ]
307259 if ! ok {
308260 return errors .Errorf ("missing realm in bearer auth challenge" )
309261 }
310262 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 )
312264 token , err := c .getBearerToken (realm , service , scope )
313265 if err != nil {
314266 return err
315267 }
316268 req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
317269 return nil
318270 }
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 )
321272}
322273
323274func (c * dockerClient ) getBearerToken (realm , service , scope string ) (string , error ) {
@@ -428,15 +379,34 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
428379}
429380
430381type 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 ()]
434404}
435405
436406func (c * dockerClient ) ping () (* pingResponse , error ) {
437407 ping := func (scheme string ) (* pingResponse , error ) {
438408 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 {} )
440410 logrus .Debugf ("Ping %s err %#v" , url , err )
441411 if err != nil {
442412 return nil , err
@@ -446,8 +416,10 @@ func (c *dockerClient) ping() (*pingResponse, error) {
446416 if resp .StatusCode != http .StatusOK && resp .StatusCode != http .StatusUnauthorized {
447417 return nil , errors .Errorf ("error pinging repository, response code %d" , resp .StatusCode )
448418 }
419+ chs := parseAuthHeader (resp .Header )
420+ c .saveChallenges (chs , resp .Request .URL )
421+
449422 pr := & pingResponse {}
450- pr .WWWAuthenticate = resp .Header .Get ("WWW-Authenticate" )
451423 pr .APIVersion = resp .Header .Get ("Docker-Distribution-Api-Version" )
452424 pr .scheme = scheme
453425 return pr , nil
@@ -464,7 +436,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
464436 // best effort to understand if we're talking to a V1 registry
465437 pingV1 := func (scheme string ) bool {
466438 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 {} )
468440 logrus .Debugf ("Ping %s err %#v" , url , err )
469441 if err != nil {
470442 return false
0 commit comments