diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 527cd1bd..217f1ca4 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -11223,6 +11223,7 @@ func TestPullRange(t *testing.T) { resp, err = resty.R().Get(loc) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusNoContent) + So(resp.Header().Get("Range"), ShouldEqual, "0-0") content := []byte("0123456789") digest := godigest.FromBytes(content) diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 1d3bffe5..a17961ae 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -1553,7 +1553,7 @@ func (rh *RouteHandler) GetBlobUpload(response http.ResponseWriter, request *htt if err != nil { details := zerr.GetDetails(err) //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - if errors.Is(err, zerr.ErrBadUploadRange) || errors.Is(err, zerr.ErrBadBlobDigest) { + if errors.Is(err, zerr.ErrBadBlobDigest) { details["session_id"] = sessionID e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) @@ -1574,7 +1574,13 @@ func (rh *RouteHandler) GetBlobUpload(response http.ResponseWriter, request *htt } response.Header().Set("Location", getBlobUploadSessionLocation(request.URL, sessionID)) - response.Header().Set("Range", fmt.Sprintf("0-%d", size-1)) + // Match POST new-upload Range for empty progress; otherwise 0..size-1 per dist-spec upload status. + rangeEnd := "0-0" + if size > 0 { + rangeEnd = fmt.Sprintf("0-%d", size-1) + } + + response.Header().Set("Range", rangeEnd) response.WriteHeader(http.StatusNoContent) } @@ -1688,7 +1694,9 @@ func (rh *RouteHandler) PatchBlobUpload(response http.ResponseWriter, request *h // @Success 201 "created" // @Header 201 {string} Location "/v2/{name}/blobs/{digest}" // @Header 201 {string} Docker-Content-Digest "Digest of the committed blob" +// @Failure 400 {string} string "bad request" // @Failure 404 {string} string "not found" +// @Failure 416 {string} string "range not satisfiable" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{session_id} [put]. func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request *http.Request) { @@ -1758,7 +1766,10 @@ func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request * to = contentLen } else if from, to, err = getContentRange(request); err != nil { // finish chunked upload - response.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + details := zerr.GetDetails(err) + details["session_id"] = sessionID + e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) + zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, apiErr.NewErrorList(e)) return } @@ -1769,7 +1780,7 @@ func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request * if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain details["session_id"] = sessionID e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) - zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) + zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { details["name"] = name e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) @@ -1805,7 +1816,7 @@ finish: } else if errors.Is(err, zerr.ErrBadUploadRange) { details["session_id"] = sessionID e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details) - zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e)) + zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, apiErr.NewErrorList(e)) } else if errors.Is(err, zerr.ErrRepoNotFound) { details["name"] = name e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details) @@ -2216,15 +2227,28 @@ func (rh *RouteHandler) OpenIDCodeExchangeCallbackWithProvider(providerName stri // helper routines func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) { - contentRange := r.Header.Get("Content-Range") - tokens := strings.Split(contentRange, "-") + contentRange := strings.TrimSpace(r.Header.Get("Content-Range")) + if contentRange == "" { + return -1, -1, zerr.ErrBadUploadRange + } - rangeStart, err := strconv.ParseInt(tokens[0], 10, 64) + startStr, endStr, ok := strings.Cut(contentRange, "-") + if !ok { + return -1, -1, zerr.ErrBadUploadRange + } + + startStr = strings.TrimSpace(startStr) + endStr = strings.TrimSpace(endStr) + if startStr == "" || endStr == "" { + return -1, -1, zerr.ErrBadUploadRange + } + + rangeStart, err := strconv.ParseInt(startStr, 10, 64) if err != nil { return -1, -1, zerr.ErrBadUploadRange } - rangeEnd, err := strconv.ParseInt(tokens[1], 10, 64) + rangeEnd, err := strconv.ParseInt(endStr, 10, 64) if err != nil { return -1, -1, zerr.ErrBadUploadRange } diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go index 05da2575..6c13ca9a 100644 --- a/pkg/api/routes_test.go +++ b/pkg/api/routes_test.go @@ -929,23 +929,8 @@ func TestRoutes(t *testing.T) { return resp.StatusCode } - // ErrBadUploadRange - statusCode := testGetBlobUpload( - []struct{ k, v string }{}, - map[string]string{}, - map[string]string{ - "name": "test", - "session_id": "1234", - }, - &mocks.MockedImageStore{ - GetBlobUploadFn: func(repo, uuid string) (int64, error) { - return 0, zerr.ErrBadUploadRange - }, - }) - So(statusCode, ShouldEqual, http.StatusBadRequest) - // ErrBadBlobDigest - statusCode = testGetBlobUpload( + statusCode := testGetBlobUpload( []struct{ k, v string }{ {"mount", "1234"}, }, @@ -1195,6 +1180,23 @@ func TestRoutes(t *testing.T) { ) So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + // Malformed Content-Range (no hyphen): must return 416, not panic. + status = testUpdateBlobUpload( + []struct{ k, v string }{ + {"digest", "sha256:7b8437f04f83f084b7ed68ad8c4a4947e12fc4e1b006b38129bac89114ec3621"}, + }, + map[string]string{ + "Content-Length": "100", + "Content-Range": "100", + }, + map[string]string{ + "name": "repo", + "session_id": "test", + }, + &mocks.MockedImageStore{}, + ) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) + status = testUpdateBlobUpload( []struct{ k, v string }{ {"digest", "sha256:7b8437f04f83f084b7ed68ad8c4a4947e12fc4e1b006b38129bac89114ec3621"}, @@ -1213,7 +1215,7 @@ func TestRoutes(t *testing.T) { }, }, ) - So(status, ShouldEqual, http.StatusBadRequest) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) status = testUpdateBlobUpload( []struct{ k, v string }{ @@ -1316,7 +1318,7 @@ func TestRoutes(t *testing.T) { }, }, ) - So(status, ShouldEqual, http.StatusBadRequest) + So(status, ShouldEqual, http.StatusRequestedRangeNotSatisfiable) status = testUpdateBlobUpload( []struct{ k, v string }{ diff --git a/swagger/docs.go b/swagger/docs.go index 518e8535..dba617e8 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -438,12 +438,24 @@ const docTemplate = `{ } } }, + "400": { + "description": "bad request", + "schema": { + "type": "string" + } + }, "404": { "description": "not found", "schema": { "type": "string" } }, + "416": { + "description": "range not satisfiable", + "schema": { + "type": "string" + } + }, "500": { "description": "internal server error", "schema": { diff --git a/swagger/swagger.json b/swagger/swagger.json index 87f9ce57..110bbb14 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -430,12 +430,24 @@ } } }, + "400": { + "description": "bad request", + "schema": { + "type": "string" + } + }, "404": { "description": "not found", "schema": { "type": "string" } }, + "416": { + "description": "range not satisfiable", + "schema": { + "type": "string" + } + }, "500": { "description": "internal server error", "schema": { diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index f1b41a9b..c08f418d 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -674,10 +674,18 @@ paths: Location: description: /v2/{name}/blobs/{digest} type: string + "400": + description: bad request + schema: + type: string "404": description: not found schema: type: string + "416": + description: range not satisfiable + schema: + type: string "500": description: internal server error schema: