Skip to content

Commit 4e59d5f

Browse files
committed
docker: mimic docker upstream registry authentication
Signed-off-by: Antonio Murdaca <[email protected]>
1 parent b5c6de3 commit 4e59d5f

File tree

5 files changed

+111
-98
lines changed

5 files changed

+111
-98
lines changed

docker/docker_client.go

Lines changed: 34 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
4545

4646
// dockerClient is configuration for dealing with a single Docker registry.
4747
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
5656
}
5757

5858
// this is cloned from docker/go-connections because upstream docker has changed
@@ -187,27 +187,31 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool)
187187
}, nil
188188
}
189189

190+
type requestOptions struct {
191+
remoteName string
192+
actions string
193+
}
194+
190195
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
191196
// 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) {
197+
func (c *dockerClient) makeRequest(method, url string, headers map[string][]string, stream io.Reader, opts requestOptions) (*http.Response, error) {
193198
if c.scheme == "" {
194199
pr, err := c.ping()
195200
if err != nil {
196201
return nil, err
197202
}
198-
c.wwwAuthenticate = pr.WWWAuthenticate
199203
c.scheme = pr.scheme
200204
}
201205

202206
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
203-
return c.makeRequestToResolvedURL(method, url, headers, stream, -1, true)
207+
return c.makeRequestToResolvedURL(method, url, headers, stream, -1, true, opts)
204208
}
205209

206210
// makeRequestToResolvedURL creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
207211
// streamLen, if not -1, specifies the length of the data expected on stream.
208212
// makeRequest should generally be preferred.
209213
// 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) {
214+
func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[string][]string, stream io.Reader, streamLen int64, sendAuth bool, opts requestOptions) (*http.Response, error) {
211215
req, err := http.NewRequest(method, url, stream)
212216
if err != nil {
213217
return nil, err
@@ -224,8 +228,8 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
224228
if c.ctx != nil && c.ctx.DockerRegistryUserAgent != "" {
225229
req.Header.Add("User-Agent", c.ctx.DockerRegistryUserAgent)
226230
}
227-
if c.wwwAuthenticate != "" && sendAuth {
228-
if err := c.setupRequestAuth(req); err != nil {
231+
if sendAuth {
232+
if err := c.setupRequestAuth(req, opts.remoteName, opts.actions); err != nil {
229233
return nil, err
230234
}
231235
}
@@ -237,87 +241,31 @@ func (c *dockerClient) makeRequestToResolvedURL(method, url string, headers map[
237241
return res, nil
238242
}
239243

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)
244+
func (c *dockerClient) setupRequestAuth(req *http.Request, remoteName, actions string) error {
245+
if len(c.challenges) == 0 {
246+
return nil
244247
}
245-
switch tokens[0] {
246-
case "Basic":
248+
// assume just one...
249+
challenge := c.challenges[0]
250+
switch challenge.Scheme {
251+
case "basic":
247252
req.SetBasicAuth(c.username, c.password)
248253
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-
}
254+
case "bearer":
306255
realm, ok := challenge.Parameters["realm"]
307256
if !ok {
308257
return errors.Errorf("missing realm in bearer auth challenge")
309258
}
310259
service, _ := challenge.Parameters["service"] // Will be "" if not present
311-
scope, _ := challenge.Parameters["scope"] // Will be "" if not present
260+
scope := fmt.Sprintf("repository:%s:%s", remoteName, actions)
312261
token, err := c.getBearerToken(realm, service, scope)
313262
if err != nil {
314263
return err
315264
}
316265
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
317266
return nil
318267
}
319-
return errors.Errorf("no handler for %s authentication", tokens[0])
320-
// support docker bearer with authconfig's Auth string? see docker2aci
268+
return errors.Errorf("no handler for %s authentication", challenge.Scheme)
321269
}
322270

323271
func (c *dockerClient) getBearerToken(realm, service, scope string) (string, error) {
@@ -428,15 +376,14 @@ func getAuth(ctx *types.SystemContext, registry string) (string, string, error)
428376
}
429377

430378
type pingResponse struct {
431-
WWWAuthenticate string
432-
APIVersion string
433-
scheme string
379+
APIVersion string
380+
scheme string
434381
}
435382

436383
func (c *dockerClient) ping() (*pingResponse, error) {
437384
ping := func(scheme string) (*pingResponse, error) {
438385
url := fmt.Sprintf(baseURL, scheme, c.registry)
439-
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
386+
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true, requestOptions{})
440387
logrus.Debugf("Ping %s err %#v", url, err)
441388
if err != nil {
442389
return nil, err
@@ -446,8 +393,9 @@ func (c *dockerClient) ping() (*pingResponse, error) {
446393
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
447394
return nil, errors.Errorf("error pinging repository, response code %d", resp.StatusCode)
448395
}
396+
c.challenges = parseAuthHeader(resp.Header)
397+
449398
pr := &pingResponse{}
450-
pr.WWWAuthenticate = resp.Header.Get("WWW-Authenticate")
451399
pr.APIVersion = resp.Header.Get("Docker-Distribution-Api-Version")
452400
pr.scheme = scheme
453401
return pr, nil
@@ -464,7 +412,7 @@ func (c *dockerClient) ping() (*pingResponse, error) {
464412
// best effort to understand if we're talking to a V1 registry
465413
pingV1 := func(scheme string) bool {
466414
url := fmt.Sprintf(baseURLV1, scheme, c.registry)
467-
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true)
415+
resp, err := c.makeRequestToResolvedURL("GET", url, nil, nil, -1, true, requestOptions{})
468416
logrus.Debugf("Ping %s err %#v", url, err)
469417
if err != nil {
470418
return false

docker/docker_image.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ func (i *Image) SourceRefFullName() string {
4040
// GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any.
4141
func (i *Image) GetRepositoryTags() ([]string, error) {
4242
url := fmt.Sprintf(tagsURL, i.src.ref.ref.RemoteName())
43-
res, err := i.src.c.makeRequest("GET", url, nil, nil)
43+
opts := requestOptions{
44+
remoteName: i.src.ref.ref.RemoteName(),
45+
actions: "pull",
46+
}
47+
res, err := i.src.c.makeRequest("GET", url, nil, nil, opts)
4448
if err != nil {
4549
return nil, err
4650
}

docker/docker_image_dest.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobI
9191
checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), inputInfo.Digest.String())
9292

9393
logrus.Debugf("Checking %s", checkURL)
94-
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
94+
opts := requestOptions{
95+
remoteName: d.ref.ref.RemoteName(),
96+
actions: "push",
97+
}
98+
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil, opts)
9599
if err != nil {
96100
return types.BlobInfo{}, err
97101
}
@@ -114,7 +118,11 @@ func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobI
114118
// FIXME? Chunked upload, progress reporting, etc.
115119
uploadURL := fmt.Sprintf(blobUploadURL, d.ref.ref.RemoteName())
116120
logrus.Debugf("Uploading %s", uploadURL)
117-
res, err := d.c.makeRequest("POST", uploadURL, nil, nil)
121+
opts := requestOptions{
122+
remoteName: d.ref.ref.RemoteName(),
123+
actions: "push",
124+
}
125+
res, err := d.c.makeRequest("POST", uploadURL, nil, nil, opts)
118126
if err != nil {
119127
return types.BlobInfo{}, err
120128
}
@@ -131,7 +139,7 @@ func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobI
131139
digester := digest.Canonical.Digester()
132140
sizeCounter := &sizeCounter{}
133141
tee := io.TeeReader(stream, io.MultiWriter(digester.Hash(), sizeCounter))
134-
res, err = d.c.makeRequestToResolvedURL("PATCH", uploadLocation.String(), map[string][]string{"Content-Type": {"application/octet-stream"}}, tee, inputInfo.Size, true)
142+
res, err = d.c.makeRequestToResolvedURL("PATCH", uploadLocation.String(), map[string][]string{"Content-Type": {"application/octet-stream"}}, tee, inputInfo.Size, true, opts)
135143
if err != nil {
136144
logrus.Debugf("Error uploading layer chunked, response %#v", *res)
137145
return types.BlobInfo{}, err
@@ -150,7 +158,7 @@ func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobI
150158
// TODO: check inputInfo.Digest == computedDigest https://github.com/containers/image/pull/70#discussion_r77646717
151159
locationQuery.Set("digest", computedDigest.String())
152160
uploadLocation.RawQuery = locationQuery.Encode()
153-
res, err = d.c.makeRequestToResolvedURL("PUT", uploadLocation.String(), map[string][]string{"Content-Type": {"application/octet-stream"}}, nil, -1, true)
161+
res, err = d.c.makeRequestToResolvedURL("PUT", uploadLocation.String(), map[string][]string{"Content-Type": {"application/octet-stream"}}, nil, -1, true, opts)
154162
if err != nil {
155163
return types.BlobInfo{}, err
156164
}
@@ -171,7 +179,11 @@ func (d *dockerImageDestination) HasBlob(info types.BlobInfo) (bool, int64, erro
171179
checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), info.Digest.String())
172180

173181
logrus.Debugf("Checking %s", checkURL)
174-
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
182+
opts := requestOptions{
183+
remoteName: d.ref.ref.RemoteName(),
184+
actions: "push",
185+
}
186+
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil, opts)
175187
if err != nil {
176188
return false, -1, err
177189
}
@@ -215,7 +227,11 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
215227
if mimeType != "" {
216228
headers["Content-Type"] = []string{mimeType}
217229
}
218-
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(m))
230+
opts := requestOptions{
231+
remoteName: d.ref.ref.RemoteName(),
232+
actions: "push",
233+
}
234+
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(m), opts)
219235
if err != nil {
220236
return err
221237
}

docker/docker_image_src.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,15 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
8181

8282
func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) {
8383
url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), tagOrDigest)
84+
//fmt.Println(s.ref.ref.RemoteName())
85+
//return nil, "", errors.New("FUCK")
8486
headers := make(map[string][]string)
8587
headers["Accept"] = s.requestedManifestMIMETypes
86-
res, err := s.c.makeRequest("GET", url, headers, nil)
88+
opts := requestOptions{
89+
remoteName: s.ref.ref.RemoteName(),
90+
actions: "pull",
91+
}
92+
res, err := s.c.makeRequest("GET", url, headers, nil, opts)
8793
if err != nil {
8894
return nil, "", err
8995
}
@@ -135,9 +141,13 @@ func (s *dockerImageSource) getExternalBlob(urls []string) (io.ReadCloser, int64
135141
var (
136142
resp *http.Response
137143
err error
144+
opts = requestOptions{
145+
remoteName: s.ref.ref.RemoteName(),
146+
actions: "pull",
147+
}
138148
)
139149
for _, url := range urls {
140-
resp, err = s.c.makeRequestToResolvedURL("GET", url, nil, nil, -1, false)
150+
resp, err = s.c.makeRequestToResolvedURL("GET", url, nil, nil, -1, false, opts)
141151
if err == nil {
142152
if resp.StatusCode != http.StatusOK {
143153
err = errors.Errorf("error fetching external blob from %q: %d", url, resp.StatusCode)
@@ -168,7 +178,11 @@ func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64,
168178

169179
url := fmt.Sprintf(blobsURL, s.ref.ref.RemoteName(), info.Digest.String())
170180
logrus.Debugf("Downloading %s", url)
171-
res, err := s.c.makeRequest("GET", url, nil, nil)
181+
opts := requestOptions{
182+
remoteName: s.ref.ref.RemoteName(),
183+
actions: "pull",
184+
}
185+
res, err := s.c.makeRequest("GET", url, nil, nil, opts)
172186
if err != nil {
173187
return nil, 0, err
174188
}
@@ -265,7 +279,11 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
265279
return err
266280
}
267281
getURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), reference)
268-
get, err := c.makeRequest("GET", getURL, headers, nil)
282+
opts := requestOptions{
283+
remoteName: ref.ref.RemoteName(),
284+
actions: "pull",
285+
}
286+
get, err := c.makeRequest("GET", getURL, headers, nil, opts)
269287
if err != nil {
270288
return err
271289
}
@@ -287,7 +305,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
287305

288306
// When retrieving the digest from a registry >= 2.3 use the following header:
289307
// "Accept": "application/vnd.docker.distribution.manifest.v2+json"
290-
delete, err := c.makeRequest("DELETE", deleteURL, headers, nil)
308+
delete, err := c.makeRequest("DELETE", deleteURL, headers, nil, opts)
291309
if err != nil {
292310
return err
293311
}

docker/wwwauthenticate.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,36 @@ package docker
44

55
import (
66
"net/http"
7+
"net/url"
78
"strings"
89
)
910

11+
func normalizeURL(endpoint *url.URL) {
12+
endpoint.Host = strings.ToLower(endpoint.Host)
13+
endpoint.Host = canonicalAddr(endpoint)
14+
}
15+
16+
// FROM: https://golang.org/src/net/http/http.go
17+
// Given a string of the form "host", "host:port", or "[ipv6::address]:port",
18+
// return true if the string includes a port.
19+
func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") }
20+
21+
// FROM: http://golang.org/src/net/http/transport.go
22+
var portMap = map[string]string{
23+
"http": "80",
24+
"https": "443",
25+
}
26+
27+
// canonicalAddr returns url.Host but always with a ":port" suffix
28+
// FROM: http://golang.org/src/net/http/transport.go
29+
func canonicalAddr(url *url.URL) string {
30+
addr := url.Host
31+
if !hasPort(addr) {
32+
return addr + ":" + portMap[url.Scheme]
33+
}
34+
return addr
35+
}
36+
1037
// challenge carries information from a WWW-Authenticate response header.
1138
// See RFC 7235.
1239
type challenge struct {

0 commit comments

Comments
 (0)