From 568d7ac810b63291e731d1b6b6842d7e6605ed25 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 2 Jul 2021 19:05:43 +0300 Subject: [PATCH 1/2] [#95] Supported get match headers Supported If-Match and If-None-Match. Signed-off-by: Denis Kirillov --- api/handler/get.go | 35 +++++++++++--- api/handler/get_test.go | 100 ++++++++++++++++++++++++++++++++++++++++ api/headers.go | 2 + 3 files changed, 130 insertions(+), 7 deletions(-) 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" From 95476b1c9ca04b5daa4dbf94f0d3bd1c2684a099 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Fri, 2 Jul 2021 19:21:53 +0300 Subject: [PATCH 2/2] [#95] Supported copy match headers Supported x-amz-copy-source-if-match and x-amz-copy-source-if-none-match. Signed-off-by: Denis Kirillov --- api/handler/copy.go | 19 +++++++++---------- api/handler/get.go | 14 +++++++++----- api/handler/get_test.go | 30 +++++++++++++++--------------- api/headers.go | 2 ++ 4 files changed, 35 insertions(+), 30 deletions(-) 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 65475d46..7958b23e 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -13,13 +13,17 @@ 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) { const prefix = "bytes=" rangeHeader := headers.Get("Range") @@ -92,7 +96,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { return } - status := checkGetPreconditions(inf, args) + status := checkPreconditions(inf, args.Conditional) if status != http.StatusOK { w.WriteHeader(status) return @@ -118,7 +122,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { } } -func checkGetPreconditions(inf *layer.ObjectInfo, args *getObjectArgs) int { +func checkPreconditions(inf *layer.ObjectInfo, args *conditionalArgs) int { if len(args.IfMatch) > 0 && args.IfMatch != inf.HashSum { return http.StatusPreconditionFailed } @@ -139,7 +143,7 @@ func checkGetPreconditions(inf *layer.ObjectInfo, args *getObjectArgs) int { func parseGetObjectArgs(headers http.Header) (*getObjectArgs, error) { var err error - args := &getObjectArgs{ + args := &conditionalArgs{ IfMatch: headers.Get(api.IfMatch), IfNoneMatch: headers.Get(api.IfNoneMatch), } @@ -151,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 7a6b55b6..9ebc51d5 100644 --- a/api/handler/get_test.go +++ b/api/handler/get_test.go @@ -57,84 +57,84 @@ func TestPreconditions(t *testing.T) { for _, tc := range []struct { name string info *layer.ObjectInfo - args *getObjectArgs + args *conditionalArgs expected int }{ { name: "no conditions", info: new(layer.ObjectInfo), - args: new(getObjectArgs), + args: new(conditionalArgs), expected: http.StatusOK, }, { name: "IfMatch true", info: newInfo(etag, today), - args: &getObjectArgs{IfMatch: etag}, + args: &conditionalArgs{IfMatch: etag}, expected: http.StatusOK, }, { name: "IfMatch false", info: newInfo(etag, today), - args: &getObjectArgs{IfMatch: etag2}, + args: &conditionalArgs{IfMatch: etag2}, expected: http.StatusPreconditionFailed}, { name: "IfNoneMatch true", info: newInfo(etag, today), - args: &getObjectArgs{IfNoneMatch: etag2}, + args: &conditionalArgs{IfNoneMatch: etag2}, expected: http.StatusOK}, { name: "IfNoneMatch false", info: newInfo(etag, today), - args: &getObjectArgs{IfNoneMatch: etag}, + args: &conditionalArgs{IfNoneMatch: etag}, expected: http.StatusNotModified}, { name: "IfModifiedSince true", info: newInfo(etag, today), - args: &getObjectArgs{IfModifiedSince: &yesterday}, + args: &conditionalArgs{IfModifiedSince: &yesterday}, expected: http.StatusOK}, { name: "IfModifiedSince false", info: newInfo(etag, yesterday), - args: &getObjectArgs{IfModifiedSince: &today}, + args: &conditionalArgs{IfModifiedSince: &today}, expected: http.StatusNotModified}, { name: "IfUnmodifiedSince true", info: newInfo(etag, yesterday), - args: &getObjectArgs{IfUnmodifiedSince: &today}, + args: &conditionalArgs{IfUnmodifiedSince: &today}, expected: http.StatusOK}, { name: "IfUnmodifiedSince false", info: newInfo(etag, today), - args: &getObjectArgs{IfUnmodifiedSince: &yesterday}, + args: &conditionalArgs{IfUnmodifiedSince: &yesterday}, expected: http.StatusPreconditionFailed}, { name: "IfMatch true, IfUnmodifiedSince false", info: newInfo(etag, today), - args: &getObjectArgs{IfMatch: etag, IfUnmodifiedSince: &yesterday}, + args: &conditionalArgs{IfMatch: etag, IfUnmodifiedSince: &yesterday}, expected: http.StatusOK, }, { name: "IfMatch false, IfUnmodifiedSince true", info: newInfo(etag, yesterday), - args: &getObjectArgs{IfMatch: etag2, IfUnmodifiedSince: &today}, + args: &conditionalArgs{IfMatch: etag2, IfUnmodifiedSince: &today}, expected: http.StatusPreconditionFailed, }, { name: "IfNoneMatch false, IfModifiedSince true", info: newInfo(etag, today), - args: &getObjectArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday}, + args: &conditionalArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday}, expected: http.StatusNotModified, }, { name: "IfNoneMatch true, IfModifiedSince false", info: newInfo(etag, yesterday), - args: &getObjectArgs{IfNoneMatch: etag2, IfModifiedSince: &today}, + args: &conditionalArgs{IfNoneMatch: etag2, IfModifiedSince: &today}, expected: http.StatusNotModified, }, } { t.Run(tc.name, func(t *testing.T) { - actual := checkGetPreconditions(tc.info, tc.args) + actual := checkPreconditions(tc.info, tc.args) require.Equal(t, tc.expected, actual) }) } diff --git a/api/headers.go b/api/headers.go index 81782f6d..e983a342 100644 --- a/api/headers.go +++ b/api/headers.go @@ -29,4 +29,6 @@ const ( 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" )