| package handlers |
| |
| import ( |
| "fmt" |
| "net/http" |
| "net/url" |
| |
| "github.com/docker/distribution" |
| dcontext "github.com/docker/distribution/context" |
| "github.com/docker/distribution/reference" |
| "github.com/docker/distribution/registry/api/errcode" |
| "github.com/docker/distribution/registry/api/v2" |
| "github.com/docker/distribution/registry/storage" |
| "github.com/gorilla/handlers" |
| "github.com/opencontainers/go-digest" |
| ) |
| |
| // blobUploadDispatcher constructs and returns the blob upload handler for the |
| // given request context. |
| func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler { |
| buh := &blobUploadHandler{ |
| Context: ctx, |
| UUID: getUploadUUID(ctx), |
| } |
| |
| handler := handlers.MethodHandler{ |
| "GET": http.HandlerFunc(buh.GetUploadStatus), |
| "HEAD": http.HandlerFunc(buh.GetUploadStatus), |
| } |
| |
| if !ctx.readOnly { |
| handler["POST"] = http.HandlerFunc(buh.StartBlobUpload) |
| handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData) |
| handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete) |
| handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload) |
| } |
| |
| if buh.UUID != "" { |
| state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state")) |
| if err != nil { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| dcontext.GetLogger(ctx).Infof("error resolving upload: %v", err) |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) |
| }) |
| } |
| buh.State = state |
| |
| if state.Name != ctx.Repository.Named().Name() { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| dcontext.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Named().Name()) |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) |
| }) |
| } |
| |
| if state.UUID != buh.UUID { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| dcontext.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID) |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) |
| }) |
| } |
| |
| blobs := ctx.Repository.Blobs(buh) |
| upload, err := blobs.Resume(buh, buh.UUID) |
| if err != nil { |
| dcontext.GetLogger(ctx).Errorf("error resolving upload: %v", err) |
| if err == distribution.ErrBlobUploadUnknown { |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err)) |
| }) |
| } |
| |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| }) |
| } |
| buh.Upload = upload |
| |
| if size := upload.Size(); size != buh.State.Offset { |
| defer upload.Close() |
| dcontext.GetLogger(ctx).Errorf("upload resumed at wrong offest: %d != %d", size, buh.State.Offset) |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) |
| upload.Cancel(buh) |
| }) |
| } |
| return closeResources(handler, buh.Upload) |
| } |
| |
| return handler |
| } |
| |
| // blobUploadHandler handles the http blob upload process. |
| type blobUploadHandler struct { |
| *Context |
| |
| // UUID identifies the upload instance for the current request. Using UUID |
| // to key blob writers since this implementation uses UUIDs. |
| UUID string |
| |
| Upload distribution.BlobWriter |
| |
| State blobUploadState |
| } |
| |
| // StartBlobUpload begins the blob upload process and allocates a server-side |
| // blob writer session, optionally mounting the blob from a separate repository. |
| func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) { |
| var options []distribution.BlobCreateOption |
| |
| fromRepo := r.FormValue("from") |
| mountDigest := r.FormValue("mount") |
| |
| if mountDigest != "" && fromRepo != "" { |
| opt, err := buh.createBlobMountOption(fromRepo, mountDigest) |
| if opt != nil && err == nil { |
| options = append(options, opt) |
| } |
| } |
| |
| blobs := buh.Repository.Blobs(buh) |
| upload, err := blobs.Create(buh, options...) |
| |
| if err != nil { |
| if ebm, ok := err.(distribution.ErrBlobMounted); ok { |
| if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| } |
| } else if err == distribution.ErrUnsupported { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported) |
| } else { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| } |
| return |
| } |
| |
| buh.Upload = upload |
| |
| if err := buh.blobUploadResponse(w, r, true); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| return |
| } |
| |
| w.Header().Set("Docker-Upload-UUID", buh.Upload.ID()) |
| w.WriteHeader(http.StatusAccepted) |
| } |
| |
| // GetUploadStatus returns the status of a given upload, identified by id. |
| func (buh *blobUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) { |
| if buh.Upload == nil { |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) |
| return |
| } |
| |
| // TODO(dmcgowan): Set last argument to false in blobUploadResponse when |
| // resumable upload is supported. This will enable returning a non-zero |
| // range for clients to begin uploading at an offset. |
| if err := buh.blobUploadResponse(w, r, true); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| return |
| } |
| |
| w.Header().Set("Docker-Upload-UUID", buh.UUID) |
| w.WriteHeader(http.StatusNoContent) |
| } |
| |
| // PatchBlobData writes data to an upload. |
| func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Request) { |
| if buh.Upload == nil { |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) |
| return |
| } |
| |
| ct := r.Header.Get("Content-Type") |
| if ct != "" && ct != "application/octet-stream" { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("Bad Content-Type"))) |
| // TODO(dmcgowan): encode error |
| return |
| } |
| |
| // TODO(dmcgowan): support Content-Range header to seek and write range |
| |
| if err := copyFullPayload(buh, w, r, buh.Upload, -1, "blob PATCH"); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error())) |
| return |
| } |
| |
| if err := buh.blobUploadResponse(w, r, false); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| return |
| } |
| |
| w.WriteHeader(http.StatusAccepted) |
| } |
| |
| // PutBlobUploadComplete takes the final request of a blob upload. The |
| // request may include all the blob data or no blob data. Any data |
| // provided is received and verified. If successful, the blob is linked |
| // into the blob store and 201 Created is returned with the canonical |
| // url of the blob. |
| func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *http.Request) { |
| if buh.Upload == nil { |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) |
| return |
| } |
| |
| dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters! |
| |
| if dgstStr == "" { |
| // no digest? return error, but allow retry. |
| buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest missing")) |
| return |
| } |
| |
| dgst, err := digest.Parse(dgstStr) |
| if err != nil { |
| // no digest? return error, but allow retry. |
| buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed")) |
| return |
| } |
| |
| if err := copyFullPayload(buh, w, r, buh.Upload, -1, "blob PUT"); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error())) |
| return |
| } |
| |
| desc, err := buh.Upload.Commit(buh, distribution.Descriptor{ |
| Digest: dgst, |
| |
| // TODO(stevvooe): This isn't wildly important yet, but we should |
| // really set the mediatype. For now, we can let the backend take care |
| // of this. |
| }) |
| |
| if err != nil { |
| switch err := err.(type) { |
| case distribution.ErrBlobInvalidDigest: |
| buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) |
| case errcode.Error: |
| buh.Errors = append(buh.Errors, err) |
| default: |
| switch err { |
| case distribution.ErrAccessDenied: |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeDenied) |
| case distribution.ErrUnsupported: |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported) |
| case distribution.ErrBlobInvalidLength, distribution.ErrBlobDigestUnsupported: |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) |
| default: |
| dcontext.GetLogger(buh).Errorf("unknown error completing upload: %v", err) |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| } |
| |
| } |
| |
| // Clean up the backend blob data if there was an error. |
| if err := buh.Upload.Cancel(buh); err != nil { |
| // If the cleanup fails, all we can do is observe and report. |
| dcontext.GetLogger(buh).Errorf("error canceling upload after error: %v", err) |
| } |
| |
| return |
| } |
| if err := buh.writeBlobCreatedHeaders(w, desc); err != nil { |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| return |
| } |
| } |
| |
| // CancelBlobUpload cancels an in-progress upload of a blob. |
| func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Request) { |
| if buh.Upload == nil { |
| buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) |
| return |
| } |
| |
| w.Header().Set("Docker-Upload-UUID", buh.UUID) |
| if err := buh.Upload.Cancel(buh); err != nil { |
| dcontext.GetLogger(buh).Errorf("error encountered canceling upload: %v", err) |
| buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) |
| } |
| |
| w.WriteHeader(http.StatusNoContent) |
| } |
| |
| // blobUploadResponse provides a standard request for uploading blobs and |
| // chunk responses. This sets the correct headers but the response status is |
| // left to the caller. The fresh argument is used to ensure that new blob |
| // uploads always start at a 0 offset. This allows disabling resumable push by |
| // always returning a 0 offset on check status. |
| func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error { |
| // TODO(stevvooe): Need a better way to manage the upload state automatically. |
| buh.State.Name = buh.Repository.Named().Name() |
| buh.State.UUID = buh.Upload.ID() |
| buh.Upload.Close() |
| buh.State.Offset = buh.Upload.Size() |
| buh.State.StartedAt = buh.Upload.StartedAt() |
| |
| token, err := hmacKey(buh.Config.HTTP.Secret).packUploadState(buh.State) |
| if err != nil { |
| dcontext.GetLogger(buh).Infof("error building upload state token: %s", err) |
| return err |
| } |
| |
| uploadURL, err := buh.urlBuilder.BuildBlobUploadChunkURL( |
| buh.Repository.Named(), buh.Upload.ID(), |
| url.Values{ |
| "_state": []string{token}, |
| }) |
| if err != nil { |
| dcontext.GetLogger(buh).Infof("error building upload url: %s", err) |
| return err |
| } |
| |
| endRange := buh.Upload.Size() |
| if endRange > 0 { |
| endRange = endRange - 1 |
| } |
| |
| w.Header().Set("Docker-Upload-UUID", buh.UUID) |
| w.Header().Set("Location", uploadURL) |
| |
| w.Header().Set("Content-Length", "0") |
| w.Header().Set("Range", fmt.Sprintf("0-%d", endRange)) |
| |
| return nil |
| } |
| |
| // mountBlob attempts to mount a blob from another repository by its digest. If |
| // successful, the blob is linked into the blob store and 201 Created is |
| // returned with the canonical url of the blob. |
| func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) { |
| dgst, err := digest.Parse(mountDigest) |
| if err != nil { |
| return nil, err |
| } |
| |
| ref, err := reference.WithName(fromRepo) |
| if err != nil { |
| return nil, err |
| } |
| |
| canonical, err := reference.WithDigest(ref, dgst) |
| if err != nil { |
| return nil, err |
| } |
| |
| return storage.WithMountFrom(canonical), nil |
| } |
| |
| // writeBlobCreatedHeaders writes the standard headers describing a newly |
| // created blob. A 201 Created is written as well as the canonical URL and |
| // blob digest. |
| func (buh *blobUploadHandler) writeBlobCreatedHeaders(w http.ResponseWriter, desc distribution.Descriptor) error { |
| ref, err := reference.WithDigest(buh.Repository.Named(), desc.Digest) |
| if err != nil { |
| return err |
| } |
| blobURL, err := buh.urlBuilder.BuildBlobURL(ref) |
| if err != nil { |
| return err |
| } |
| |
| w.Header().Set("Location", blobURL) |
| w.Header().Set("Content-Length", "0") |
| w.Header().Set("Docker-Content-Digest", desc.Digest.String()) |
| w.WriteHeader(http.StatusCreated) |
| return nil |
| } |