mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Beforeea29dffaa5, the image create endpoint had a [fallback for very old client versions][1] that would send authentication as body instead of through the `X-Registry-Auth` header. However, the implementation of this fallback did not handle empty bodies, resulting in an `io.EOF` error to be returned when trying to parse the body as JSON. In practice, this problem didn't happen when using the CLI, because even if no authentication was present, `registry.EncodeAuthConfig()` (used by the CLI to set the `X-Registry-Auth` header) would produce an empty JSON document (`{}`), which would be encoded in base64 (`e30=`), so we would never set an empty `X-Registry-Auth` (but other clients may have hit this situation). That behavior was unexpected, because not all registries require authentication, and omitting the `X-Registry-Auth` should be valid. We also want to have more flexibility in authentication (and being able to distinguish unauthenticated requests, so that we can fallback to alternative paths). Unfortunately, we can't change existing daemons, so must account for the faulty fallback. Currently, omitting the `X-Registry-Auth` produces an error, but we can avoid this by unconditionally sending a body, which may be an empty JSON document (`{}`). I explored possible options for this; we can either construct our own empty JSON (`json.RawMessage("{}")`) to be explicit that we're sending empty JSON, but [`encodeBody()`][2] is currently hard-coded to expect JSON requests, and unconditionally calls [`encodeData`][3], which encodes to JSON, so we may as well take advantage of `http.NoBody`, which gets marshaled to an empty JSON document; https://go.dev/play/p/QCw9dJ6LGQu package main import ( "encoding/json" "fmt" "net/http" ) func main() { body, _ := json.Marshal(http.NoBody) fmt.Println(string(body)) } Before this patch, a client omitting `X-Registry-Auth` (and no body) would produce an error; docker pull -q busybox docker tag busybox 127.0.0.1:5001/myimage:latest docker run -d --name registry -p 127.0.0.1:5001:5000 registry:3 docker push 127.0.0.1:5001/myimage:latest Error response from daemon: bad parameters and missing X-Registry-Auth: invalid X-Registry-Auth header: EOF With this patch applied, no error is produced; docker pull -q busybox docker tag busybox 127.0.0.1:5001/myimage:latest docker run -d --name registry -p 127.0.0.1:5001:5000 registry:3 docker push 127.0.0.1:5001/myimage:latest The push refers to repository [127.0.0.1:5001/myimage] 189fdd150837: Pushed latest: digest: sha256:68a0d55a75c935e1101d16ded1c748babb7f96a9af43f7533ba83b87e2508b82 size: 610 [1]:63fcf7d858/api/types/registry/authconfig_test.go (L109-L114)[2]:63fcf7d858/client/request.go (L67-L87)[3]:63fcf7d858/client/request.go (L296-L304)[4]:ea29dffaa5Signed-off-by: Sebastiaan van Stijn <github@gone.nl> (cherry picked from commitb1ce0c89f0) Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/api/types/registry"
|
|
)
|
|
|
|
// ImagePush requests the docker host to push an image to a remote registry.
|
|
// It executes the privileged function if the operation is unauthorized
|
|
// and it tries one more time.
|
|
// It's up to the caller to handle the io.ReadCloser and close it properly.
|
|
func (cli *Client) ImagePush(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error) {
|
|
ref, err := reference.ParseNormalizedNamed(image)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, isCanonical := ref.(reference.Canonical); isCanonical {
|
|
return nil, errors.New("cannot push a digest reference")
|
|
}
|
|
|
|
query := url.Values{}
|
|
if !options.All {
|
|
ref = reference.TagNameOnly(ref)
|
|
if tagged, ok := ref.(reference.Tagged); ok {
|
|
query.Set("tag", tagged.Tag())
|
|
}
|
|
}
|
|
|
|
if options.Platform != nil {
|
|
if err := cli.NewVersionError(ctx, "1.46", "platform"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := *options.Platform
|
|
pJson, err := json.Marshal(p)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid platform: %v", err)
|
|
}
|
|
|
|
query.Set("platform", string(pJson))
|
|
}
|
|
|
|
resp, err := cli.tryImagePush(ctx, ref.Name(), query, options.RegistryAuth)
|
|
if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil {
|
|
newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx)
|
|
if privilegeErr != nil {
|
|
return nil, privilegeErr
|
|
}
|
|
resp, err = cli.tryImagePush(ctx, ref.Name(), query, newAuthHeader)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Body, nil
|
|
}
|
|
|
|
func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (*http.Response, error) {
|
|
// Always send a body (which may be an empty JSON document ("{}")) to prevent
|
|
// EOF errors on older daemons which had faulty fallback code for handling
|
|
// authentication in the body when no auth-header was set, resulting in;
|
|
//
|
|
// Error response from daemon: bad parameters and missing X-Registry-Auth: invalid X-Registry-Auth header: EOF
|
|
//
|
|
// We use [http.NoBody], which gets marshaled to an empty JSON document.
|
|
//
|
|
// see: https://github.com/moby/moby/commit/ea29dffaa541289591aa44fa85d2a596ce860e16
|
|
return cli.post(ctx, "/images/"+imageID+"/push", query, http.NoBody, http.Header{
|
|
registry.AuthHeader: {registryAuth},
|
|
})
|
|
}
|