1
0
mirror of https://github.com/distribution/distribution synced 2024-11-12 05:45:51 +01:00

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:
wang yan 2020-08-19 17:07:38 +08:00 committed by Wang Yan
parent b459aa2391
commit d7a2b14489
4 changed files with 96 additions and 8 deletions

@ -32,6 +32,17 @@ var (
HTTPStatusCode: http.StatusBadRequest, 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 // ErrorCodeNameInvalid is returned when the name in the manifest does not
// match the provided name. // match the provided name.
ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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) uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
finishUpload(t, env.builder, imageName, uploadURLBase, dgst) 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. // Use a head request to see if the layer exists.
resp, err = http.Head(layerURL) 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") 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) u, err := url.Parse(uploadURLBase)
if err != nil { if err != nil {
t.Fatalf("unexpected error parsing pushLayer url: %v", err) 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() uploadURL := u.String()
digester := digest.Canonical.Digester() req, err := http.NewRequest("PATCH", uploadURL, body)
req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash()))
if err != nil { if err != nil {
t.Fatalf("unexpected error creating new request: %v", err) t.Fatalf("unexpected error creating new request: %v", err)
} }
req.Header.Set("Content-Type", "application/octet-stream") req.Header.Set("Content-Type", "application/octet-stream")
if options.contentRange != "" {
req.Header.Set("Content-Range", options.contentRange)
}
resp, err := http.DefaultClient.Do(req) 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) { 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 { if err != nil {
t.Fatalf("unexpected error doing push layer request: %v", err) 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"}, "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) { func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
dcontext "github.com/distribution/distribution/v3/context" dcontext "github.com/distribution/distribution/v3/context"
@ -133,7 +134,29 @@ func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Reque
return 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 { if err := copyFullPayload(buh, w, r, buh.Upload, -1, "blob PATCH"); err != nil {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error())) buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error()))

@ -3,8 +3,11 @@ package handlers
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings"
dcontext "github.com/distribution/distribution/v3/context" dcontext "github.com/distribution/distribution/v3/context"
) )
@ -64,3 +67,20 @@ func copyFullPayload(ctx context.Context, responseWriter http.ResponseWriter, r
return nil 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
}