add content range handling in patch blob
Fixes #3141 1, return 416 for Out-of-order blob upload 2, return 400 for content length and content size mismatch Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
b459aa2391
commit
d7a2b14489
4 changed files with 96 additions and 8 deletions
|
@ -32,6 +32,17 @@ var (
|
|||
HTTPStatusCode: http.StatusBadRequest,
|
||||
})
|
||||
|
||||
// ErrorCodeRangeInvalid is returned when uploading a blob if the provided
|
||||
// content range is invalid.
|
||||
ErrorCodeRangeInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
||||
Value: "RANGE_INVALID",
|
||||
Message: "invalid content range",
|
||||
Description: `When a layer is uploaded, the provided range is checked
|
||||
against the uploaded chunk. This error is returned if the range is
|
||||
out of order.`,
|
||||
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
})
|
||||
|
||||
// ErrorCodeNameInvalid is returned when the name in the manifest does not
|
||||
// match the provided name.
|
||||
ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
||||
|
|
|
@ -533,6 +533,32 @@ func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
|
|||
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
|
||||
finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
|
||||
|
||||
// -----------------------------------------
|
||||
// Do layer push with invalid content range
|
||||
layerFile.Seek(0, io.SeekStart)
|
||||
uploadURLBase, _ = startPushLayer(t, env, imageName)
|
||||
sizeInvalid := chunkOptions{
|
||||
contentRange: "0-20",
|
||||
}
|
||||
resp, err = doPushChunk(t, uploadURLBase, layerFile, sizeInvalid)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
checkResponse(t, "putting size invalid chunk", resp, http.StatusBadRequest)
|
||||
|
||||
layerFile.Seek(0, io.SeekStart)
|
||||
uploadURLBase, _ = startPushLayer(t, env, imageName)
|
||||
outOfOrder := chunkOptions{
|
||||
contentRange: "3-22",
|
||||
}
|
||||
resp, err = doPushChunk(t, uploadURLBase, layerFile, outOfOrder)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
checkResponse(t, "putting range out of order chunk", resp, http.StatusRequestedRangeNotSatisfiable)
|
||||
|
||||
// ------------------------
|
||||
// Use a head request to see if the layer exists.
|
||||
resp, err = http.Head(layerURL)
|
||||
|
@ -2242,7 +2268,12 @@ func finishUpload(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadU
|
|||
return resp.Header.Get("Location")
|
||||
}
|
||||
|
||||
func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) {
|
||||
type chunkOptions struct {
|
||||
// Content-Range header to set when pushing chunks
|
||||
contentRange string
|
||||
}
|
||||
|
||||
func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader, options chunkOptions) (*http.Response, error) {
|
||||
u, err := url.Parse(uploadURLBase)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing pushLayer url: %v", err)
|
||||
|
@ -2254,21 +2285,24 @@ func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Resp
|
|||
|
||||
uploadURL := u.String()
|
||||
|
||||
digester := digest.Canonical.Digester()
|
||||
|
||||
req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash()))
|
||||
req, err := http.NewRequest("PATCH", uploadURL, body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating new request: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
if options.contentRange != "" {
|
||||
req.Header.Set("Content-Range", options.contentRange)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
|
||||
return resp, digester.Digest(), err
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) {
|
||||
resp, dgst, err := doPushChunk(t, uploadURLBase, body)
|
||||
digester := digest.Canonical.Digester()
|
||||
|
||||
resp, err := doPushChunk(t, uploadURLBase, io.TeeReader(body, digester.Hash()), chunkOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error doing push layer request: %v", err)
|
||||
}
|
||||
|
@ -2285,7 +2319,7 @@ func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLB
|
|||
"Content-Length": []string{"0"},
|
||||
})
|
||||
|
||||
return resp.Header.Get("Location"), dgst
|
||||
return resp.Header.Get("Location"), digester.Digest()
|
||||
}
|
||||
|
||||
func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
dcontext "github.com/distribution/distribution/v3/context"
|
||||
|
@ -133,7 +134,29 @@ func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): support Content-Range header to seek and write range
|
||||
cr := r.Header.Get("Content-Range")
|
||||
cl := r.Header.Get("Content-Length")
|
||||
if cr != "" && cl != "" {
|
||||
start, end, err := parseContentRange(cr)
|
||||
if err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
||||
return
|
||||
}
|
||||
if start > end || start != buh.Upload.Size() {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeRangeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
clInt, err := strconv.ParseInt(cl, 10, 64)
|
||||
if err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
||||
return
|
||||
}
|
||||
if clInt != (end-start)+1 {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeSizeInvalid)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := copyFullPayload(buh, w, r, buh.Upload, -1, "blob PATCH"); err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))
|
||||
|
|
|
@ -3,8 +3,11 @@ package handlers
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dcontext "github.com/distribution/distribution/v3/context"
|
||||
)
|
||||
|
@ -64,3 +67,20 @@ func copyFullPayload(ctx context.Context, responseWriter http.ResponseWriter, r
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseContentRange(cr string) (int64, int64, error) {
|
||||
ranges := strings.Split(cr, "-")
|
||||
if len(ranges) != 2 {
|
||||
return -1, -1, fmt.Errorf("invalid content range format, %s", cr)
|
||||
}
|
||||
start, err := strconv.ParseInt(ranges[0], 10, 64)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
end, err := strconv.ParseInt(ranges[1], 10, 64)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
return start, end, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue