8fbc1de081
When a client disconnects without completing a HTTP request, we were attempting to process the partial request, which usually leads to a 400 error. These errors can pollute the logs and make it more difficult to track down real bugs. This change uses CloseNotifier to detect disconnects. In combination with checking Content-Length, we can detect a disconnect before sending the full payload, and avoid logging a 400 error. This logic is only applied to PUT, POST, and PATCH endpoints, as these are the places where disconnects during a request are most likely to happen. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
272 lines
8.3 KiB
Go
272 lines
8.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/docker/distribution"
|
|
ctxu "github.com/docker/distribution/context"
|
|
"github.com/docker/distribution/digest"
|
|
"github.com/docker/distribution/manifest"
|
|
"github.com/docker/distribution/registry/api/errcode"
|
|
"github.com/docker/distribution/registry/api/v2"
|
|
"github.com/gorilla/handlers"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// imageManifestDispatcher takes the request context and builds the
|
|
// appropriate handler for handling image manifest requests.
|
|
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
|
|
imageManifestHandler := &imageManifestHandler{
|
|
Context: ctx,
|
|
}
|
|
reference := getReference(ctx)
|
|
dgst, err := digest.ParseDigest(reference)
|
|
if err != nil {
|
|
// We just have a tag
|
|
imageManifestHandler.Tag = reference
|
|
} else {
|
|
imageManifestHandler.Digest = dgst
|
|
}
|
|
|
|
return handlers.MethodHandler{
|
|
"GET": http.HandlerFunc(imageManifestHandler.GetImageManifest),
|
|
"PUT": http.HandlerFunc(imageManifestHandler.PutImageManifest),
|
|
"DELETE": http.HandlerFunc(imageManifestHandler.DeleteImageManifest),
|
|
}
|
|
}
|
|
|
|
// imageManifestHandler handles http operations on image manifests.
|
|
type imageManifestHandler struct {
|
|
*Context
|
|
|
|
// One of tag or digest gets set, depending on what is present in context.
|
|
Tag string
|
|
Digest digest.Digest
|
|
}
|
|
|
|
// GetImageManifest fetches the image manifest from the storage backend, if it exists.
|
|
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
|
|
ctxu.GetLogger(imh).Debug("GetImageManifest")
|
|
manifests, err := imh.Repository.Manifests(imh)
|
|
if err != nil {
|
|
imh.Errors = append(imh.Errors, err)
|
|
return
|
|
}
|
|
|
|
var sm *manifest.SignedManifest
|
|
if imh.Tag != "" {
|
|
sm, err = manifests.GetByTag(imh.Tag)
|
|
} else {
|
|
if etagMatch(r, imh.Digest.String()) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
sm, err = manifests.Get(imh.Digest)
|
|
}
|
|
|
|
if err != nil {
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
|
|
return
|
|
}
|
|
|
|
// Get the digest, if we don't already have it.
|
|
if imh.Digest == "" {
|
|
dgst, err := digestManifest(imh, sm)
|
|
if err != nil {
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
|
|
return
|
|
}
|
|
if etagMatch(r, dgst.String()) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
|
|
imh.Digest = dgst
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("Content-Length", fmt.Sprint(len(sm.Raw)))
|
|
w.Header().Set("Docker-Content-Digest", imh.Digest.String())
|
|
w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
|
|
w.Write(sm.Raw)
|
|
}
|
|
|
|
func etagMatch(r *http.Request, etag string) bool {
|
|
for _, headerVal := range r.Header["If-None-Match"] {
|
|
if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// PutImageManifest validates and stores and image in the registry.
|
|
func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) {
|
|
ctxu.GetLogger(imh).Debug("PutImageManifest")
|
|
manifests, err := imh.Repository.Manifests(imh)
|
|
if err != nil {
|
|
imh.Errors = append(imh.Errors, err)
|
|
return
|
|
}
|
|
|
|
// Get a channel that tells us if the client disconnects
|
|
var clientClosed <-chan bool
|
|
if notifier, ok := w.(http.CloseNotifier); ok {
|
|
clientClosed = notifier.CloseNotify()
|
|
} else {
|
|
panic("the ResponseWriter does not implement CloseNotifier")
|
|
}
|
|
|
|
// Copy the data
|
|
jsonBytes, err := ioutil.ReadAll(r.Body)
|
|
if clientClosed != nil && (err != nil || (r.ContentLength > 0 && int64(len(jsonBytes)) < r.ContentLength)) {
|
|
// Didn't recieve as much content as expected. Did the client
|
|
// disconnect during the request? If so, avoid returning a 400
|
|
// error to keep the logs cleaner.
|
|
select {
|
|
case <-clientClosed:
|
|
ctxu.GetLogger(imh).Error("client disconnected during image manifest PUT")
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
if err != nil {
|
|
ctxu.GetLogger(imh).Errorf("unknown error reading payload: %v", err)
|
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
|
return
|
|
}
|
|
|
|
var manifest manifest.SignedManifest
|
|
if err := json.Unmarshal(jsonBytes, &manifest); err != nil {
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
|
return
|
|
}
|
|
|
|
dgst, err := digestManifest(imh, &manifest)
|
|
if err != nil {
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
|
|
return
|
|
}
|
|
|
|
// Validate manifest tag or digest matches payload
|
|
if imh.Tag != "" {
|
|
if manifest.Tag != imh.Tag {
|
|
ctxu.GetLogger(imh).Errorf("invalid tag on manifest payload: %q != %q", manifest.Tag, imh.Tag)
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid)
|
|
return
|
|
}
|
|
|
|
imh.Digest = dgst
|
|
} else if imh.Digest != "" {
|
|
if dgst != imh.Digest {
|
|
ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", dgst, imh.Digest)
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
|
|
return
|
|
}
|
|
} else {
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified"))
|
|
return
|
|
}
|
|
|
|
if err := manifests.Put(&manifest); err != nil {
|
|
// TODO(stevvooe): These error handling switches really need to be
|
|
// handled by an app global mapper.
|
|
switch err := err.(type) {
|
|
case distribution.ErrManifestVerification:
|
|
for _, verificationError := range err {
|
|
switch verificationError := verificationError.(type) {
|
|
case distribution.ErrManifestBlobUnknown:
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeBlobUnknown.WithDetail(verificationError.Digest))
|
|
case distribution.ErrManifestUnverified:
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified)
|
|
default:
|
|
if verificationError == digest.ErrDigestInvalidFormat {
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
|
|
} else {
|
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Construct a canonical url for the uploaded manifest.
|
|
location, err := imh.urlBuilder.BuildManifestURL(imh.Repository.Name(), imh.Digest.String())
|
|
if err != nil {
|
|
// NOTE(stevvooe): Given the behavior above, this absurdly unlikely to
|
|
// happen. We'll log the error here but proceed as if it worked. Worst
|
|
// case, we set an empty location header.
|
|
ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err)
|
|
}
|
|
|
|
w.Header().Set("Location", location)
|
|
w.Header().Set("Docker-Content-Digest", imh.Digest.String())
|
|
w.WriteHeader(http.StatusCreated)
|
|
}
|
|
|
|
// DeleteImageManifest removes the manifest with the given digest from the registry.
|
|
func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) {
|
|
ctxu.GetLogger(imh).Debug("DeleteImageManifest")
|
|
|
|
manifests, err := imh.Repository.Manifests(imh)
|
|
if err != nil {
|
|
imh.Errors = append(imh.Errors, err)
|
|
return
|
|
}
|
|
|
|
err = manifests.Delete(imh.Digest)
|
|
if err != nil {
|
|
switch err {
|
|
case digest.ErrDigestUnsupported:
|
|
case digest.ErrDigestInvalidFormat:
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
|
|
return
|
|
case distribution.ErrBlobUnknown:
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
case distribution.ErrUnsupported:
|
|
imh.Errors = append(imh.Errors, v2.ErrorCodeUnsupported)
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
default:
|
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
// digestManifest takes a digest of the given manifest. This belongs somewhere
|
|
// better but we'll wait for a refactoring cycle to find that real somewhere.
|
|
func digestManifest(ctx context.Context, sm *manifest.SignedManifest) (digest.Digest, error) {
|
|
p, err := sm.Payload()
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "missing signature key") {
|
|
ctxu.GetLogger(ctx).Errorf("error getting manifest payload: %v", err)
|
|
return "", err
|
|
}
|
|
|
|
// NOTE(stevvooe): There are no signatures but we still have a
|
|
// payload. The request will fail later but this is not the
|
|
// responsibility of this part of the code.
|
|
p = sm.Raw
|
|
}
|
|
|
|
dgst, err := digest.FromBytes(p)
|
|
if err != nil {
|
|
ctxu.GetLogger(ctx).Errorf("error digesting manifest: %v", err)
|
|
return "", err
|
|
}
|
|
|
|
return dgst, err
|
|
}
|