diff --git a/api/handler/put_test.go b/api/handler/put_test.go index 57982670..de22bbf8 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -489,6 +489,58 @@ func TestPutObjectWithStreamEmptyBodyAWSExample(t *testing.T) { require.Empty(t, res.Contents[0].Size) } +func TestPutObjectWithStreamEmptyBody(t *testing.T) { + hc := prepareHandlerContext(t) + + bktName := "bucket" + createTestBucket(hc, bktName) + + t.Run("unsigned", func(t *testing.T) { + t.Run("trailer", func(t *testing.T) { + objName := "unsigned trailer" + + w, req := getEmptyChunkedRequestUnsigned(hc.context, t, bktName, objName) + req.Header.Del(api.ContentType) + hc.Handler().PutObjectHandler(w, req) + assertStatus(t, w, http.StatusOK) + + d, h := getObject(hc, bktName, objName) + require.Empty(t, d) + require.Equal(t, "0", h.Get(api.ContentLength)) + }) + }) + + t.Run("sigv4", func(t *testing.T) { + t.Run("no trailer", func(t *testing.T) { + objName := "sigv4 no trailer" + + w, req := getEmptyChunkedRequest(hc.context, t, bktName, objName) + req.Header.Del(api.ContentType) + hc.Handler().PutObjectHandler(w, req) + assertStatus(t, w, http.StatusOK) + + d, h := getObject(hc, bktName, objName) + require.Empty(t, d) + require.Equal(t, "0", h.Get(api.ContentLength)) + }) + }) + + t.Run("sigv4a", func(t *testing.T) { + t.Run("trailer", func(t *testing.T) { + objName := "sigv4a trailer" + + w, req := getEmptyChunkedRequestSigv4a(hc.context, t, bktName, objName) + req.Header.Del(api.ContentType) + hc.Handler().PutObjectHandler(w, req) + assertStatus(t, w, http.StatusOK) + + d, h := getObject(hc, bktName, objName) + require.Empty(t, d) + require.Equal(t, "0", h.Get(api.ContentLength)) + }) + }) +} + func TestPutChunkedTestContentEncoding(t *testing.T) { hc := prepareHandlerContext(t) @@ -818,6 +870,83 @@ func getEmptyChunkedRequest(ctx context.Context, t *testing.T, bktName, objName return w, req } +func getEmptyChunkedRequestUnsigned(ctx context.Context, t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request) { + AWSAccessKeyID := "3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt" + AWSSecretAccessKey := "f1a0d650b650149f1a83140418e88a3c5572a0103e912e326492a91c19c4488a" + + reqBody := bytes.NewBufferString("0\r\nx-amz-checksum-crc64nvme:AAAAAAAAAAA=\r\n\r\n") + + req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, reqBody) + require.NoError(t, err) + req.Header.Set(api.Authorization, "AWS4-HMAC-SHA256 Credential=3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt/20250213/ru/s3/aws4_request, SignedHeaders=content-encoding;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-sdk-checksum-algorithm;x-amz-trailer, Signature=1231b012c0ac313770c5a95ccf77b95b6c9b1c3760d6aa24cb8309801d56eb4a") + req.Header.Set(api.ContentEncoding, api.AwsChunked) + req.Header.Set(api.AmzDate, "20250213T124858Z") + req.Header.Set(api.AmzContentSha256, api.StreamingUnsignedPayloadTrailer) + req.Header.Set(api.AmzDecodedContentLength, "0") + req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc64nvme") + req.Header.Set("X-Amz-Sdk-Checksum-Algorithm", "CRC64NVME") + + signTime, err := time.Parse("20060102T150405Z", req.Header.Get(api.AmzDate)) + require.NoError(t, err) + + w := httptest.NewRecorder() + reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "") + req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo)) + req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{ + ClientTime: signTime, + AuthHeaders: &middleware.AuthHeader{ + AccessKeyID: AWSAccessKeyID, + SignatureV4: "1231b012c0ac313770c5a95ccf77b95b6c9b1c3760d6aa24cb8309801d56eb4a", + Region: "ru", + }, + AccessBox: &accessbox.Box{Gate: &accessbox.GateData{SecretKey: AWSSecretAccessKey}}, + })) + + return w, req +} + +func getEmptyChunkedRequestSigv4a(ctx context.Context, t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request) { + AWSAccessKeyID := "3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt" + AWSSecretAccessKey := "f1a0d650b650149f1a83140418e88a3c5572a0103e912e326492a91c19c4488a" + + body := "0;chunk-signature=3046022100ab9229a80d70f4d004768992881821a441a4ad4102e18de567e68216659bf497022100ec47a7a445351683557eedf893e6ed250c97af4b0415814671770b83766d69be\r\n" + + "x-amz-checksum-crc32:AAAAAA==\r\n" + + "x-amz-trailer-signature:3046022100a0a66c1adcee8d99460b4631b23c95fbad9eb4e6c56f1afb9e255715ba141169022100b2cfc8adc8036eb985f1ab0e770b575284c5fc8ca75c226558d3142cbaab83ce\r\n\r\n" + + req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, bytes.NewBufferString(body)) + require.NoError(t, err) + req.Header.Set(api.Authorization, "AWS4-ECDSA-P256-SHA256 Credential=3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt/20250213/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-region-set;x-amz-sdk-checksum-algorithm;x-amz-trailer, Signature=304402202e1f1efcc56c588d9a94a3d8f20368686df8bfd5e8aad01fc4eff569ff38f1800220215198e3f1ba785492fe6703c4722872909ce8a09e8c9a13da90a9230c7a24b7") + req.Header.Set("Amz-Sdk-Invocation-Id", "d42dc16d-7899-55fb-5b72-a654bd482f4f") + req.Header.Set("Amz-Sdk-Request", "attempt=1; max=2") + req.Header.Set(api.ContentEncoding, api.AwsChunked) + req.Header.Set(api.AmzDate, "20250213T132401Z") + req.Header.Set(api.AmzContentSha256, api.StreamingContentV4aSHA256Trailer) + req.Header.Set(api.AmzDecodedContentLength, "0") + req.Header.Set(api.ContentLength, "367") + req.Header.Set(api.ContentType, "text/plain: charset=UTF-8") + req.Header.Set("X-Amz-Region-Set", "use-east-1") + req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc32") + req.Header.Set("X-Amz-Sdk-Checksum-Algorithm", "CRC32") + + signTime, err := time.Parse("20060102T150405Z", req.Header.Get(api.AmzDate)) + require.NoError(t, err) + + w := httptest.NewRecorder() + reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "") + req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo)) + req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{ + ClientTime: signTime, + AuthHeaders: &middleware.AuthHeader{ + AccessKeyID: AWSAccessKeyID, + SignatureV4: "304402202e1f1efcc56c588d9a94a3d8f20368686df8bfd5e8aad01fc4eff569ff38f1800220215198e3f1ba785492fe6703c4722872909ce8a09e8c9a13da90a9230c7a24b7", + Region: "us-east-1", + }, + AccessBox: &accessbox.Box{Gate: &accessbox.GateData{SecretKey: AWSSecretAccessKey}}, + })) + + return w, req +} + func TestCreateBucket(t *testing.T) { hc := prepareHandlerContext(t) bktName := "bkt-name" diff --git a/api/handler/s3reader.go b/api/handler/s3reader.go index 0cd474e1..83aa3399 100644 --- a/api/handler/s3reader.go +++ b/api/handler/s3reader.go @@ -59,6 +59,10 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) { buf = buf[num:] } + if c.err != nil { + return 0, c.err + } + var size int for { b, err := c.reader.ReadByte() diff --git a/api/handler/s3unsignedreader.go b/api/handler/s3unsignedreader.go index efd8049e..18b71e62 100644 --- a/api/handler/s3unsignedreader.go +++ b/api/handler/s3unsignedreader.go @@ -31,6 +31,10 @@ func (c *s3UnsignedChunkReader) Read(buf []byte) (num int, err error) { buf = buf[num:] } + if c.err != nil { + return 0, c.err + } + var size int var b byte for { diff --git a/api/handler/s3v4aReader.go b/api/handler/s3v4aReader.go index b20afaab..6e33ca61 100644 --- a/api/handler/s3v4aReader.go +++ b/api/handler/s3v4aReader.go @@ -46,6 +46,10 @@ func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) { buf = buf[num:] } + if c.err != nil { + return 0, c.err + } + var size int for { b, err := c.reader.ReadByte()