diff --git a/api/handler/get.go b/api/handler/get.go index 1cb62fb0..65475d46 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -16,6 +16,8 @@ import ( type getObjectArgs struct { IfModifiedSince *time.Time IfUnmodifiedSince *time.Time + IfMatch string + IfNoneMatch string } func fetchRangeHeader(headers http.Header, fullSize uint64) (*layer.RangeParams, error) { @@ -90,12 +92,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 := checkGetPreconditions(inf, args) + if status != http.StatusOK { + w.WriteHeader(status) return } @@ -119,9 +118,31 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { } } +func checkGetPreconditions(inf *layer.ObjectInfo, args *getObjectArgs) 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 := &getObjectArgs{ + IfMatch: headers.Get(api.IfMatch), + IfNoneMatch: headers.Get(api.IfNoneMatch), + } if args.IfModifiedSince, err = parseHTTPTime(headers.Get(api.IfModifiedSince)); err != nil { return nil, err diff --git a/api/handler/get_test.go b/api/handler/get_test.go index 69bc5bd9..7a6b55b6 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 *getObjectArgs + expected int + }{ + { + name: "no conditions", + info: new(layer.ObjectInfo), + args: new(getObjectArgs), + expected: http.StatusOK, + }, + { + name: "IfMatch true", + info: newInfo(etag, today), + args: &getObjectArgs{IfMatch: etag}, + expected: http.StatusOK, + }, + { + name: "IfMatch false", + info: newInfo(etag, today), + args: &getObjectArgs{IfMatch: etag2}, + expected: http.StatusPreconditionFailed}, + { + name: "IfNoneMatch true", + info: newInfo(etag, today), + args: &getObjectArgs{IfNoneMatch: etag2}, + expected: http.StatusOK}, + { + name: "IfNoneMatch false", + info: newInfo(etag, today), + args: &getObjectArgs{IfNoneMatch: etag}, + expected: http.StatusNotModified}, + { + name: "IfModifiedSince true", + info: newInfo(etag, today), + args: &getObjectArgs{IfModifiedSince: &yesterday}, + expected: http.StatusOK}, + { + name: "IfModifiedSince false", + info: newInfo(etag, yesterday), + args: &getObjectArgs{IfModifiedSince: &today}, + expected: http.StatusNotModified}, + { + name: "IfUnmodifiedSince true", + info: newInfo(etag, yesterday), + args: &getObjectArgs{IfUnmodifiedSince: &today}, + expected: http.StatusOK}, + { + name: "IfUnmodifiedSince false", + info: newInfo(etag, today), + args: &getObjectArgs{IfUnmodifiedSince: &yesterday}, + expected: http.StatusPreconditionFailed}, + + { + name: "IfMatch true, IfUnmodifiedSince false", + info: newInfo(etag, today), + args: &getObjectArgs{IfMatch: etag, IfUnmodifiedSince: &yesterday}, + expected: http.StatusOK, + }, + { + name: "IfMatch false, IfUnmodifiedSince true", + info: newInfo(etag, yesterday), + args: &getObjectArgs{IfMatch: etag2, IfUnmodifiedSince: &today}, + expected: http.StatusPreconditionFailed, + }, + { + name: "IfNoneMatch false, IfModifiedSince true", + info: newInfo(etag, today), + args: &getObjectArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday}, + expected: http.StatusNotModified, + }, + { + name: "IfNoneMatch true, IfModifiedSince false", + info: newInfo(etag, yesterday), + args: &getObjectArgs{IfNoneMatch: etag2, IfModifiedSince: &today}, + expected: http.StatusNotModified, + }, + } { + t.Run(tc.name, func(t *testing.T) { + actual := checkGetPreconditions(tc.info, tc.args) + require.Equal(t, tc.expected, actual) + }) + } +} diff --git a/api/headers.go b/api/headers.go index 735ebb69..81782f6d 100644 --- a/api/headers.go +++ b/api/headers.go @@ -24,6 +24,8 @@ 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"