From a4f05e0fc8cbca5ac776d3dcbf59c859eaeca2c5 Mon Sep 17 00:00:00 2001 From: yminer Date: Fri, 29 Mar 2024 07:20:47 +0000 Subject: [PATCH] upload blob by chunk should return 416 Signed-off-by: yminer --- src/lib/errors/const.go | 2 + src/lib/http/error.go | 1 + .../middleware/blob/patch_blob_upload.go | 76 +++++++++++++++++++ tests/ci/conformance_test.sh | 3 +- 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/lib/errors/const.go b/src/lib/errors/const.go index 1ba2256fe6b..a40cbf390b6 100644 --- a/src/lib/errors/const.go +++ b/src/lib/errors/const.go @@ -19,6 +19,8 @@ const ( NotFoundCode = "NOT_FOUND" // ConflictCode ... ConflictCode = "CONFLICT" + // RangeUnsatisfy = "RequestRange_Unsatisfy" + RangeUnsatisfy = "RequestRange_Unsatisfy" // UnAuthorizedCode ... UnAuthorizedCode = "UNAUTHORIZED" // BadRequestCode ... diff --git a/src/lib/http/error.go b/src/lib/http/error.go index c3abd94ba5e..11b42b45595 100644 --- a/src/lib/http/error.go +++ b/src/lib/http/error.go @@ -43,6 +43,7 @@ var ( errors.ViolateForeignKeyConstraintCode: http.StatusPreconditionFailed, errors.PROJECTPOLICYVIOLATION: http.StatusPreconditionFailed, errors.GeneralCode: http.StatusInternalServerError, + errors.RangeUnsatisfy: http.StatusRequestedRangeNotSatisfiable, } ) diff --git a/src/server/middleware/blob/patch_blob_upload.go b/src/server/middleware/blob/patch_blob_upload.go index bec7d76e7c0..98b1879cc6d 100644 --- a/src/server/middleware/blob/patch_blob_upload.go +++ b/src/server/middleware/blob/patch_blob_upload.go @@ -15,15 +15,79 @@ package blob import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" "fmt" "net/http" "strconv" "strings" + "time" + "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/pkg/distribution" "github.com/goharbor/harbor/src/server/middleware" ) +type blobUploadState struct { + // name is the primary repository under which the blob will be linked. + Name string + + // UUID identifies the upload. + UUID string + + // offset contains the current progress of the upload. + Offset int64 + + // StartedAt is the original start time of the upload. + StartedAt time.Time +} + +// /v2/conformance/testrepo/blobs/uploads/96a6fe7e-6683-4dce-a0d5-80fdc50f3822?_state=PwxpQahplvWdosoKCWat7zK2PtCo_4pUEEAmWzV2YOl7Ik5hbWUiOiJjb25mb3JtYW5jZS90ZXN0cmVwbyIsIlVVSUQiOiI5NmE2ZmU3ZS02NjgzLTRkY2UtYTBkNS04MGZkYzUwZjM4MjIiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMjQtMDMtMTBUMTU6MzA6MjMuMjE1MjU1MzVaIn0%3D +func unpackUploadState(r *http.Request) (blobUploadState, error) { + var state blobUploadState + token := r.FormValue("_state") + tokenBytes, err := base64.URLEncoding.DecodeString(token) + if err != nil { + return state, err + } + + secret := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer") + mac := hmac.New(sha256.New, []byte(secret)) + if len(tokenBytes) < mac.Size() { + return state, err + } + macBytes := tokenBytes[:mac.Size()] + messageBytes := tokenBytes[mac.Size():] + + mac.Write(messageBytes) + if !hmac.Equal(mac.Sum(nil), macBytes) { + return state, err + } + + if err := json.Unmarshal(messageBytes, &state); err != nil { + return state, err + } + + return state, nil +} + +func isDisorder(state *blobUploadState, r *http.Request) (bool, error) { + cntRange := r.Header.Get("Content-Range") + startstr := strings.Split(cntRange, "-")[0] + offset := state.Offset + + start, err := strconv.ParseInt(startstr, 10, 64) + if err != nil { + return false, err + } + if start > offset { + return true, nil + } + return false, nil +} + // PatchBlobUploadMiddleware middleware to record the accepted blob size for stream blob upload func PatchBlobUploadMiddleware() func(http.Handler) http.Handler { return middleware.AfterResponse(func(w http.ResponseWriter, r *http.Request, statusCode int) error { @@ -31,6 +95,18 @@ func PatchBlobUploadMiddleware() func(http.Handler) http.Handler { if statusCode != http.StatusAccepted { return nil } + //check if disorder when upload by chunk + state, err := unpackUploadState(r) + if err != nil { + return err + } + disorder, err := isDisorder(&state, r) + if err != nil { + return err + } + if disorder { + return errors.New(nil).WithCode(errors.RangeUnsatisfy).WithMessage("Request Range is disordered") + } size, err := parseAcceptedBlobSize(w.Header().Get("Range")) if err != nil { diff --git a/tests/ci/conformance_test.sh b/tests/ci/conformance_test.sh index 48a419b2500..3214f1b6e7b 100755 --- a/tests/ci/conformance_test.sh +++ b/tests/ci/conformance_test.sh @@ -2,7 +2,7 @@ set -e echo "get the conformance testing code..." -git clone https://github.com/opencontainers/distribution-spec.git +# git clone https://github.com/opencontainers/distribution-spec.git function createPro { echo "create testing project: $2" @@ -31,4 +31,5 @@ export OCI_CROSSMOUNT_NAMESPACE="crossmount/testrepo" export OCI_AUTOMATIC_CROSSMOUNT="false" cd ./distribution-spec/conformance +git checkout tags/v1.1.0 go test . \ No newline at end of file