diff --git a/api/handler/copy.go b/api/handler/copy.go index 67d5ebb5..cb477639 100644 --- a/api/handler/copy.go +++ b/api/handler/copy.go @@ -13,8 +13,7 @@ import ( ) type copyObjectArgs struct { - IfModifiedSince *time.Time - IfUnmodifiedSince *time.Time + Conditional *conditionalArgs } // path2BucketObject returns bucket and object. @@ -74,12 +73,9 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if args.IfModifiedSince != nil && inf.Created.Before(*args.IfModifiedSince) { - w.WriteHeader(http.StatusNotModified) - return - } - if args.IfUnmodifiedSince != nil && inf.Created.After(*args.IfUnmodifiedSince) { - w.WriteHeader(http.StatusPreconditionFailed) + status := checkPreconditions(inf, args.Conditional) + if status != http.StatusOK { + w.WriteHeader(status) return } @@ -124,7 +120,10 @@ func writeErrorCopy(w http.ResponseWriter, r *http.Request, log *zap.Logger, msg func parseCopyObjectArgs(headers http.Header) (*copyObjectArgs, error) { var err error - args := ©ObjectArgs{} + args := &conditionalArgs{ + IfMatch: headers.Get(api.AmzCopyIfMatch), + IfNoneMatch: headers.Get(api.AmzCopyIfNoneMatch), + } if args.IfModifiedSince, err = parseHTTPTime(headers.Get(api.AmzCopyIfModifiedSince)); err != nil { return nil, err @@ -133,5 +132,5 @@ func parseCopyObjectArgs(headers http.Header) (*copyObjectArgs, error) { return nil, err } - return args, nil + return ©ObjectArgs{Conditional: args}, nil } diff --git a/api/handler/get.go b/api/handler/get.go index 1cb62fb0..7958b23e 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -13,9 +13,15 @@ import ( "go.uber.org/zap" ) -type getObjectArgs struct { +type conditionalArgs struct { IfModifiedSince *time.Time IfUnmodifiedSince *time.Time + IfMatch string + IfNoneMatch string +} + +type getObjectArgs struct { + Conditional *conditionalArgs } func fetchRangeHeader(headers http.Header, fullSize uint64) (*layer.RangeParams, error) { @@ -90,12 +96,9 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { return } - if args.IfModifiedSince != nil && inf.Created.Before(*args.IfModifiedSince) { - w.WriteHeader(http.StatusNotModified) - return - } - if args.IfUnmodifiedSince != nil && inf.Created.After(*args.IfUnmodifiedSince) { - w.WriteHeader(http.StatusPreconditionFailed) + status := checkPreconditions(inf, args.Conditional) + if status != http.StatusOK { + w.WriteHeader(status) return } @@ -119,9 +122,31 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { } } +func checkPreconditions(inf *layer.ObjectInfo, args *conditionalArgs) int { + if len(args.IfMatch) > 0 && args.IfMatch != inf.HashSum { + return http.StatusPreconditionFailed + } + if len(args.IfNoneMatch) > 0 && args.IfNoneMatch == inf.HashSum { + return http.StatusNotModified + } + if args.IfModifiedSince != nil && inf.Created.Before(*args.IfModifiedSince) { + return http.StatusNotModified + } + if args.IfUnmodifiedSince != nil && inf.Created.After(*args.IfUnmodifiedSince) { + if len(args.IfMatch) == 0 { + return http.StatusPreconditionFailed + } + } + + return http.StatusOK +} + func parseGetObjectArgs(headers http.Header) (*getObjectArgs, error) { var err error - args := &getObjectArgs{} + args := &conditionalArgs{ + IfMatch: headers.Get(api.IfMatch), + IfNoneMatch: headers.Get(api.IfNoneMatch), + } if args.IfModifiedSince, err = parseHTTPTime(headers.Get(api.IfModifiedSince)); err != nil { return nil, err @@ -130,7 +155,7 @@ func parseGetObjectArgs(headers http.Header) (*getObjectArgs, error) { return nil, err } - return args, nil + return &getObjectArgs{Conditional: args}, nil } func parseHTTPTime(data string) (*time.Time, error) { diff --git a/api/handler/get_test.go b/api/handler/get_test.go index 69bc5bd9..9ebc51d5 100644 --- a/api/handler/get_test.go +++ b/api/handler/get_test.go @@ -3,6 +3,7 @@ package handler import ( "net/http" "testing" + "time" "github.com/nspcc-dev/neofs-s3-gw/api/layer" "github.com/stretchr/testify/require" @@ -39,3 +40,102 @@ func TestFetchRangeHeader(t *testing.T) { require.Equal(t, tc.expected, params) } } + +func newInfo(etag string, created time.Time) *layer.ObjectInfo { + return &layer.ObjectInfo{ + HashSum: etag, + Created: created, + } +} + +func TestPreconditions(t *testing.T) { + today := time.Now() + yesterday := today.Add(-24 * time.Hour) + etag := "etag" + etag2 := "etag2" + + for _, tc := range []struct { + name string + info *layer.ObjectInfo + args *conditionalArgs + expected int + }{ + { + name: "no conditions", + info: new(layer.ObjectInfo), + args: new(conditionalArgs), + expected: http.StatusOK, + }, + { + name: "IfMatch true", + info: newInfo(etag, today), + args: &conditionalArgs{IfMatch: etag}, + expected: http.StatusOK, + }, + { + name: "IfMatch false", + info: newInfo(etag, today), + args: &conditionalArgs{IfMatch: etag2}, + expected: http.StatusPreconditionFailed}, + { + name: "IfNoneMatch true", + info: newInfo(etag, today), + args: &conditionalArgs{IfNoneMatch: etag2}, + expected: http.StatusOK}, + { + name: "IfNoneMatch false", + info: newInfo(etag, today), + args: &conditionalArgs{IfNoneMatch: etag}, + expected: http.StatusNotModified}, + { + name: "IfModifiedSince true", + info: newInfo(etag, today), + args: &conditionalArgs{IfModifiedSince: &yesterday}, + expected: http.StatusOK}, + { + name: "IfModifiedSince false", + info: newInfo(etag, yesterday), + args: &conditionalArgs{IfModifiedSince: &today}, + expected: http.StatusNotModified}, + { + name: "IfUnmodifiedSince true", + info: newInfo(etag, yesterday), + args: &conditionalArgs{IfUnmodifiedSince: &today}, + expected: http.StatusOK}, + { + name: "IfUnmodifiedSince false", + info: newInfo(etag, today), + args: &conditionalArgs{IfUnmodifiedSince: &yesterday}, + expected: http.StatusPreconditionFailed}, + + { + name: "IfMatch true, IfUnmodifiedSince false", + info: newInfo(etag, today), + args: &conditionalArgs{IfMatch: etag, IfUnmodifiedSince: &yesterday}, + expected: http.StatusOK, + }, + { + name: "IfMatch false, IfUnmodifiedSince true", + info: newInfo(etag, yesterday), + args: &conditionalArgs{IfMatch: etag2, IfUnmodifiedSince: &today}, + expected: http.StatusPreconditionFailed, + }, + { + name: "IfNoneMatch false, IfModifiedSince true", + info: newInfo(etag, today), + args: &conditionalArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday}, + expected: http.StatusNotModified, + }, + { + name: "IfNoneMatch true, IfModifiedSince false", + info: newInfo(etag, yesterday), + args: &conditionalArgs{IfNoneMatch: etag2, IfModifiedSince: &today}, + expected: http.StatusNotModified, + }, + } { + t.Run(tc.name, func(t *testing.T) { + actual := checkPreconditions(tc.info, tc.args) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/api/headers.go b/api/headers.go index 735ebb69..e983a342 100644 --- a/api/headers.go +++ b/api/headers.go @@ -24,7 +24,11 @@ const ( Action = "Action" IfModifiedSince = "If-Modified-Since" IfUnmodifiedSince = "If-Unmodified-Since" + IfMatch = "If-Match" + IfNoneMatch = "If-None-Match" AmzCopyIfModifiedSince = "X-Amz-Copy-Source-If-Modified-Since" AmzCopyIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since" + AmzCopyIfMatch = "X-Amz-Copy-Source-If-Match" + AmzCopyIfNoneMatch = "X-Amz-Copy-Source-If-None-Match" )