mirror of
https://github.com/versity/versitygw.git
synced 2026-09-25 01:14:22 +00:00
The rejection now reports `Content-Encoding` as the `ArgumentName` and the bare `aws-chunked` token as the `ArgumentValue`, rather than `x-amz-content-sha256` and the payload type, so a request sending `gzip,aws-chunked` gets back just the offending coding, and the message carries S3's trailing period. Adds integration tests for the three cases: `UnsignedStreamingPayloadTrailer_strips_aws_chunked_content_encoding` for a framed upload where only `aws-chunked` is dropped and the remaining codings keep their order, `PutObject_plain_stores_aws_chunked_content_encoding` for a hex-payload PUT that stores the token as sent, and `PutObject_unsigned_payload_with_aws_chunked_content_encoding` for the full error shape across four header spellings.
760 lines
26 KiB
Go
760 lines
26 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
func UnsignedStreaminPayloadTrailer_malformed_trailer(s *S3Conf) error {
|
|
testName := "UnsignedStreaminPayloadTrailer_malformed_trailer"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-obj"
|
|
for i, test := range []struct {
|
|
trailer string
|
|
decContentLength string
|
|
payload string
|
|
}{
|
|
// missing trailer in the payload
|
|
{"x-amz-checksum-crc64nvme", "5", "5\r\nhello\r\n0\r\n\r\n"},
|
|
// empty checksum key
|
|
{"x-amz-checksum-crc64nvme", "5", "5\r\nhello\r\n0\r\n:M3eFcAZSQlc=\r\n\r\n"},
|
|
// missing x-amz-trailer
|
|
{"", "5", "5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n"},
|
|
// invalid trailer in payload
|
|
{"x-amz-checksum-crc64nvme", "5", "5\r\nhello\r\n0\r\ninvalid_trailer:M3eFcAZSQlc=\r\n\r\n"},
|
|
} {
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": test.decContentLength,
|
|
}
|
|
if test.trailer != "" {
|
|
reqHeaders["x-amz-trailer"] = test.trailer
|
|
}
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, []byte(test.payload), reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrMalformedTrailer), apiErr); err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_missing_invalid_dec_content_length(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_missing_invalid_dec_content_length"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "object"
|
|
for i, clength := range []string{"", "abc", "12x"} {
|
|
reqHeaders := map[string]string{
|
|
"x-amz-trailer": "x-amz-checksum-crc64nvme",
|
|
}
|
|
if clength != "" {
|
|
reqHeaders["x-amz-decoded-content-length"] = clength
|
|
}
|
|
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrMissingContentLength), apiErr); err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_invalid_trailing_checksum(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_invalid_trailing_checksum"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "5",
|
|
"x-amz-trailer": "x-amz-checksum-crc64nvme",
|
|
}
|
|
|
|
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:invalid_checksum\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return compareS3ApiError(s3err.GetInvalidTrailingChecksumHeaderErr("x-amz-checksum-crc64nvme"), apiErr)
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "5",
|
|
"x-amz-trailer": "x-amz-checksum-crc64nvme",
|
|
}
|
|
|
|
// valid crc64nvme, but incorrect
|
|
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:QFRKMGE3tuw=\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return compareS3ApiError(s3err.GetChecksumBadDigestErr("CRC64NVME"), apiErr)
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_multiple_checksum_headers(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_multiple_checksum_headers"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n")
|
|
|
|
for i, test := range []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"crc32", "NhCmhg=="},
|
|
{"crc32c", "+Cy97w=="},
|
|
{"crc64nvme", "QFRKMGE3tuw="},
|
|
{"sha1", "qvTGHdzF6KLavt4PO0gs2a6pQ00="},
|
|
{"sha256", "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="},
|
|
{"sha512", "uiz+VuENyLjzFxWrdmbN/NdIdldj/V3saJF6FsckcYu6xu26fM0CfAaTySaoFJmIdI2m5wbYMJxtShQ1PXk3Tg=="},
|
|
{"md5", "EtWtthXtCk3RxUiXKw+ydw=="},
|
|
{"xxhash64", "McmxUfNFLUs="},
|
|
{"xxhash3", "tLh+aNltTM4="},
|
|
{"xxhash128", "MXqrLVobrQkTBf82+7i9AQ=="},
|
|
} {
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "5",
|
|
"x-amz-trailer": "x-amz-checksum-crc64nvme",
|
|
fmt.Sprintf("x-amz-checksum-%s", test.key): test.value,
|
|
}
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrMultipleChecksumHeaders), apiErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "5",
|
|
"x-amz-trailer": "x-amz-checksum-crc64nvme",
|
|
"x-amz-sdk-checksum-algorithm": "sha1",
|
|
}
|
|
|
|
// valid crc64nvme, but incorrect
|
|
body := []byte("5\r\nhello\r\n0\r\nx-amz-checksum-crc64nvme:M3eFcAZSQlc=\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return compareS3ApiError(s3err.GetInvalidChecksumHeaderErr("x-amz-sdk-checksum-algorithm"), apiErr)
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_incomplete_body(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_incomplete_body"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
for i, body := range [][]byte{
|
|
[]byte("A\ndummy data\r\n0\r\n\r\n"),
|
|
[]byte("A\r\ndummy data0\r\n\r\n"),
|
|
[]byte("A\r\nXYZ\r\ndummy data\r\n0\r\n\r\n"),
|
|
[]byte("B\r\ndummy data\r\n0\r\n\r\n"),
|
|
[]byte("A\r\ndummy data\r\n0\r\n"),
|
|
[]byte("A\r\ndummy data\r\n0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n"),
|
|
[]byte("A\r\n"),
|
|
[]byte("A\r\nA\r\ndummy data\r\n0\r\n\r\n"),
|
|
// invalid chunk size
|
|
[]byte("invalid_chunk_size\r\ndummy data\r\n0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n"),
|
|
[]byte("A\r\ndummy data\r\nJ\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n"),
|
|
} {
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "10",
|
|
"x-amz-trailer": "x-amz-checksum-crc64nvme",
|
|
}
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrIncompleteBody), apiErr); err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_invalid_chunk_size(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_invalid_chunk_size"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
chSizeErr := s3err.GetAPIError(s3err.ErrInvalidChunkSize)
|
|
for i, test := range []struct {
|
|
chunkSizes []int64
|
|
expectedErr error
|
|
}{
|
|
{[]int64{8192, 8192, 100, 0}, nil},
|
|
{[]int64{100, 0}, nil},
|
|
{[]int64{10000, 9500, 20, 0}, nil},
|
|
{[]int64{8200, 8201, 10000, 10, 0}, nil},
|
|
{[]int64{100, 8192, 100, 0}, chSizeErr},
|
|
{[]int64{8192, 10, 100, 0}, chSizeErr},
|
|
{[]int64{10, 10, 10, 0}, chSizeErr},
|
|
{[]int64{8192, 10, 100, 0}, chSizeErr},
|
|
{[]int64{10000, 10, 8192, 8192, 0}, chSizeErr},
|
|
{[]int64{8192, 10, 10, 8192, 20, 20000, 0}, chSizeErr},
|
|
} {
|
|
cLength, payload, err := constructUnsignedPaylod(test.chunkSizes...)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": fmt.Sprint(cLength),
|
|
}
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, payload, reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if test.expectedErr == nil && apiErr != nil {
|
|
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
if test.expectedErr != nil {
|
|
if apiErr == nil {
|
|
return fmt.Errorf("test %v failed: expected %w, instead got nil", i+1, test.expectedErr)
|
|
}
|
|
|
|
expErr, ok := test.expectedErr.(s3err.APIError)
|
|
if !ok {
|
|
return fmt.Errorf("test %v failed: invalid expected error type", i+1)
|
|
}
|
|
|
|
if err := compareS3ApiError(expErr, apiErr); err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_content_length_payload_size_mismatch(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_content_length_payload_size_mismatch"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
for i, test := range []struct {
|
|
payload string
|
|
cLength int64
|
|
trailer string
|
|
}{
|
|
{"b\r\nabcdefghijk\r\n0\r\n\r\n", 5, ""},
|
|
{"b\r\nabcdefghijk\r\n0\r\n\r\n", 200, ""},
|
|
{"a\r\ndummy data\r\n0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n", 128, "crc64nvme"},
|
|
{"a\r\ndummy data\r\n0\r\nx-amz-checksum-sha256:eXuwq/95jXIAr3aF3KeQHt/8Ur8mUA1b2XKCZY7iQVI=\r\n\r\n", 7, "crc64nvme"},
|
|
} {
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": fmt.Sprint(test.cLength),
|
|
}
|
|
if test.trailer != "" {
|
|
reqHeaders["x-amz-trailer"] = fmt.Sprintf("x-amz-checksum-%s", test.trailer)
|
|
}
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, []byte(test.payload), reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrContentLengthMismatch), apiErr); err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "11",
|
|
}
|
|
|
|
body := []byte("B\r\nhello world\r\n0\r\n\r\n")
|
|
|
|
headers, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if apiErr != nil {
|
|
return fmt.Errorf("%s: %s", apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
csum := headers["x-amz-checksum-crc64nvme"]
|
|
expectedCsum := "jSnVw/bqjr4="
|
|
if csum != expectedCsum {
|
|
return fmt.Errorf("expected the crc64nvme to be %s, instead got %s", expectedCsum, csum)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
body := []byte("7\r\nabcdefg\r\n0\r\n\r\n")
|
|
|
|
for i, test := range []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"crc32", "MSpqpg=="},
|
|
{"crc32c", "5if0QQ=="},
|
|
{"crc64nvme", "SmzZ/LTp1CA="},
|
|
{"sha1", "L7XhNBn8iSRoZeejJPR27GJOh0A="},
|
|
{"sha256", "fRpUEnsiJQL1t5tfsIAwYRUqRPkrN+I8ZSe69mXU2po="},
|
|
{"sha512", "1xakGIVptoqxtt+sF45XARTN8Oo6HMDjFIbD5BJBvGp2Qk6MN6sm8Jb8he+YhsjLY0GH9P3f9kX7CZ8f9UxrjA=="},
|
|
{"md5", "esZsDxSN6VGbi9JkMSxNZA=="},
|
|
{"xxhash64", "GGCUDikCgi0="},
|
|
{"xxhash3", "WkDcP9RMBS8="},
|
|
{"xxhash128", "Kq/YOGmlnDE/55jA7aptxg=="},
|
|
} {
|
|
csumHdr := fmt.Sprintf("x-amz-checksum-%s", test.key)
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "7",
|
|
csumHdr: test.value,
|
|
}
|
|
|
|
headers, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if apiErr != nil {
|
|
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
if headers[csumHdr] != test.value {
|
|
return fmt.Errorf("expected the %s to be %s, instead got %s", csumHdr, test.value, headers[csumHdr])
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
|
|
for i, test := range []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"crc32", "MSpqpg=="},
|
|
{"crc32c", "5if0QQ=="},
|
|
{"crc64nvme", "SmzZ/LTp1CA="},
|
|
{"sha1", "L7XhNBn8iSRoZeejJPR27GJOh0A="},
|
|
{"sha256", "fRpUEnsiJQL1t5tfsIAwYRUqRPkrN+I8ZSe69mXU2po="},
|
|
{"sha512", "1xakGIVptoqxtt+sF45XARTN8Oo6HMDjFIbD5BJBvGp2Qk6MN6sm8Jb8he+YhsjLY0GH9P3f9kX7CZ8f9UxrjA=="},
|
|
{"md5", "esZsDxSN6VGbi9JkMSxNZA=="},
|
|
{"xxhash64", "GGCUDikCgi0="},
|
|
{"xxhash3", "WkDcP9RMBS8="},
|
|
{"xxhash128", "Kq/YOGmlnDE/55jA7aptxg=="},
|
|
} {
|
|
csumHdr := fmt.Sprintf("x-amz-checksum-%s", test.key)
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "7",
|
|
"x-amz-sdk-checksum-algorithm": strings.ToUpper(test.key),
|
|
"x-amz-trailer": csumHdr,
|
|
}
|
|
body := bytes.NewBuffer([]byte("7\r\nabcdefg\r\n0\r\n"))
|
|
|
|
_, err := body.WriteString(fmt.Sprintf("%s:%s\r\n\r\n", csumHdr, test.value))
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
headers, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body.Bytes(), reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if apiErr != nil {
|
|
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
if headers[csumHdr] != test.value {
|
|
return fmt.Errorf("expected the %s to be %s, instead got %s", csumHdr, test.value, headers[csumHdr])
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UnsignedStreamingPayloadTrailer_strips_aws_chunked_content_encoding checks that the
|
|
// aws-chunked token is dropped from the stored Content-Encoding when the body
|
|
// really was framed in it, and that every other coding survives in order.
|
|
func UnsignedStreamingPayloadTrailer_strips_aws_chunked_content_encoding(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_strips_aws_chunked_content_encoding"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
for i, test := range []struct {
|
|
contentEncoding string
|
|
stored string
|
|
}{
|
|
// nothing is left, so no Content-Encoding is stored at all
|
|
{"aws-chunked", ""},
|
|
{"aws-chunked,gzip", "gzip"},
|
|
// the remaining codings keep their order
|
|
{"gzip,aws-chunked,br", "gzip,br"},
|
|
// a framed body doesn't make every coding transport
|
|
{"gzip", "gzip"},
|
|
} {
|
|
object := fmt.Sprintf("streaming-obj-%v", i)
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "11",
|
|
"Content-Encoding": test.contentEncoding,
|
|
}
|
|
body := []byte("B\r\nhello world\r\n0\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerObjectPut(s, bucket, object, body, reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
if apiErr != nil {
|
|
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
stored, err := getStoredContentEncoding(s3client, bucket, object)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
if stored != test.stored {
|
|
return fmt.Errorf("test %v: expected the stored content encoding to be %q, instead got %q",
|
|
i+1, test.stored, stored)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_composite_checksum(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_composite_checksum"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
mp, err := createMp(s3client, bucket, object, withChecksumType(types.ChecksumTypeComposite), withChecksum(types.ChecksumAlgorithmCrc32))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "7",
|
|
}
|
|
|
|
body := []byte("7\r\nabcdefg\r\n0\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return compareS3ApiError(s3err.GetChecksumTypeMismatchErr(types.ChecksumAlgorithmCrc32, "null"), apiErr)
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_full_object(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_UploadPart_no_trailer_composite_checksum"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
mp, err := createMp(s3client, bucket, object, withChecksumType(types.ChecksumTypeFullObject), withChecksum(types.ChecksumAlgorithmCrc32))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "7",
|
|
}
|
|
|
|
body := []byte("7\r\nabcdefg\r\n0\r\n\r\n")
|
|
|
|
headers, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if apiErr != nil {
|
|
return fmt.Errorf("(%s) %s", apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
expectedCsum := "MSpqpg=="
|
|
actualCsum := headers["x-amz-checksum-crc32"]
|
|
|
|
if expectedCsum != actualCsum {
|
|
return fmt.Errorf("expected the crc32 checksum to be %s, instead got %s", expectedCsum, actualCsum)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_UploadPart_trailer_and_mp_algo_mismatch"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
mp, err := createMp(s3client, bucket, object, withChecksumType(types.ChecksumTypeFullObject), withChecksum(types.ChecksumAlgorithmCrc32))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "7",
|
|
"x-amz-trailer": "x-amz-checksum-sha256",
|
|
}
|
|
|
|
body := []byte("7\r\nabcdefg\r\n0\r\nx-amz-checksum-sha256:fRpUEnsiJQL1t5tfsIAwYRUqRPkrN+I8ZSe69mXU2po=\r\n\r\n")
|
|
|
|
_, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body, reqHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return compareS3ApiError(s3err.GetChecksumTypeMismatchErr(types.ChecksumAlgorithmCrc32, types.ChecksumAlgorithmSha256), apiErr)
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_UploadPart_success_with_trailer"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "my-object"
|
|
|
|
for i, test := range []struct {
|
|
algo types.ChecksumAlgorithm
|
|
value string
|
|
}{
|
|
{types.ChecksumAlgorithmCrc32, "QWaN2w=="},
|
|
{types.ChecksumAlgorithmCrc32c, "R/I7iQ=="},
|
|
{types.ChecksumAlgorithmCrc64nvme, "dPVWc2vU1+Q="},
|
|
{types.ChecksumAlgorithmSha1, "YR/1TvTYOJz5gtqVFoBJBtmTibY="},
|
|
{types.ChecksumAlgorithmSha256, "eXuwq/95jXIAr3aF3KeQHt/8Ur8mUA1b2XKCZY7iQVI="},
|
|
{types.ChecksumAlgorithmSha512, "6tQCd6X50h2wXt5deAY0eKF5Xb1LLbSvkZt+Bqczz8bd1+rH+VYZSgWhjOG4zJ41K0kLmDQxuDyOeHMKqKQGNA=="},
|
|
{types.ChecksumAlgorithmMd5, "Mb+5cwrlGvc5U7pyDIZg1w=="},
|
|
{types.ChecksumAlgorithmXxhash64, "vYOmFpsGXtY="},
|
|
{types.ChecksumAlgorithmXxhash3, "9J6xx21X+f8="},
|
|
{types.ChecksumAlgorithmXxhash128, "menoCZOCZ2Acv4XMxaUR9w=="},
|
|
} {
|
|
|
|
mp, err := createMp(s3client, bucket, object, withChecksum(test.algo))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
csumHdr := checksumHeaderName(test.algo)
|
|
reqHeaders := map[string]string{
|
|
"x-amz-decoded-content-length": "10",
|
|
"x-amz-sdk-checksum-algorithm": strings.ToLower(string(test.algo)),
|
|
"x-amz-trailer": csumHdr,
|
|
}
|
|
body := bytes.NewBuffer([]byte("A\r\ndummy data\r\n0\r\n"))
|
|
|
|
_, err = body.WriteString(fmt.Sprintf("%s:%s\r\n\r\n", csumHdr, test.value))
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
headers, apiErr, err := testUnsignedStreamingPayloadTrailerUploadPart(s, bucket, object, mp.UploadId, body.Bytes(), reqHeaders)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if apiErr != nil {
|
|
return fmt.Errorf("test %v failed: (%s) %s", i+1, apiErr.Code, apiErr.Message)
|
|
}
|
|
|
|
if headers[csumHdr] != test.value {
|
|
return fmt.Errorf("expected the %s to be %s, instead got %s", csumHdr, test.value, headers[csumHdr])
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func UnsignedStreamingPayloadTrailer_not_allowed(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_not_allowed"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
// doesn't matter what data is sent in the body
|
|
body := []byte("5\r\nabcde\r\n0\r\n\r\n")
|
|
// tests a couple of bucket PUT actions, where
|
|
// STREAMING-UNSIGNED-PAYLOAD-TRAILER is not allowed
|
|
for i, query := range []string{
|
|
"cors", // PutBucketCors
|
|
"tagging", // PutBucketTagging
|
|
"object-lock", // PutObjectLockConfiguration
|
|
"ownershipControls", // PutBucketOwnership
|
|
"versioning", // PutBucketVersioning
|
|
"policy", // PutBucketPolicy
|
|
} {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf("%s/%s?%s", s.endpoint, bucket, query), bytes.NewReader(body))
|
|
if err != nil {
|
|
cancel()
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
req.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER")
|
|
req.Header.Set("x-amz-decoded-content-length", "5")
|
|
|
|
_, apiErr, err := sendSignedRequest(s, req, cancel)
|
|
if err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
|
|
if err := compareS3ApiError(s3err.GetAPIError(s3err.ErrInvalidSHA256PayloadUsage), apiErr); err != nil {
|
|
return fmt.Errorf("test %v failed: %w", i+1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// abortedChunkReader serves the first n bytes of a chunk-framed payload and
|
|
// then fails, so the transport tears the connection down mid-body.
|
|
type abortedChunkReader struct {
|
|
data []byte
|
|
pos int
|
|
}
|
|
|
|
func (r *abortedChunkReader) Read(p []byte) (int, error) {
|
|
if r.pos >= len(r.data) {
|
|
return 0, fmt.Errorf("connection aborted by test")
|
|
}
|
|
n := copy(p, r.data[r.pos:])
|
|
r.pos += n
|
|
return n, nil
|
|
}
|
|
|
|
// UnsignedStreamingPayloadTrailer_aborted_connection checks that an aws-chunked
|
|
// upload whose connection dies in the middle of a chunk leaves no object.
|
|
//
|
|
// UnsignedStreamingPayloadTrailer_incomplete_body already covers malformed and
|
|
// truncated framing, but every one of those is a COMPLETE request: the body is
|
|
// short, Content-Length agrees with it, and the chunk reader rejects what it
|
|
// parses. This one is the other shape - the framing is valid and the bytes
|
|
// simply stop arriving, which is what a client that dies or cancels looks like
|
|
// on the wire. That is the shape the plain path got wrong.
|
|
//
|
|
// The chunk readers already handled it, so this is a regression guard: the
|
|
// Content-Length check added for plain bodies must not change this path.
|
|
//
|
|
// NOTE: the request must really take the chunked path. The gateway decides
|
|
// that from x-amz-content-sha256 alone, so an UNSIGNED-PAYLOAD request with
|
|
// Content-Encoding: aws-chunked stays on the plain path - a test written that
|
|
// way silently duplicates the plain one instead of covering this.
|
|
func UnsignedStreamingPayloadTrailer_aborted_connection(s *S3Conf) error {
|
|
testName := "UnsignedStreamingPayloadTrailer_aborted_connection"
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
object := "aborted-streaming"
|
|
|
|
decoded, payload, err := constructUnsignedPaylod(65536)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to construct the payload: %w", err)
|
|
}
|
|
full := append(payload, []byte("0\r\nx-amz-checksum-crc64nvme:dPVWc2vU1+Q=\r\n\r\n")...)
|
|
|
|
// Stop well inside the first chunk: the header has been parsed, the
|
|
// data has not finished arriving.
|
|
cut := len(full) / 4
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut,
|
|
fmt.Sprintf("%s/%s/%s", s.endpoint, bucket, object),
|
|
&abortedChunkReader{data: full[:cut]})
|
|
if err != nil {
|
|
cancel()
|
|
return fmt.Errorf("failed to create a request: %w", err)
|
|
}
|
|
// Announce the whole framed payload, send a quarter of it.
|
|
req.ContentLength = int64(len(full))
|
|
req.Header.Set("x-amz-content-sha256", "STREAMING-UNSIGNED-PAYLOAD-TRAILER")
|
|
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc64nvme")
|
|
req.Header.Set("x-amz-decoded-content-length", fmt.Sprintf("%v", decoded))
|
|
|
|
signer := v4.NewSigner()
|
|
if err := signer.SignHTTP(req.Context(),
|
|
aws.Credentials{AccessKeyID: s.awsID, SecretAccessKey: s.awsSecret},
|
|
req, "STREAMING-UNSIGNED-PAYLOAD-TRAILER", "s3", s.awsRegion, time.Now()); err != nil {
|
|
cancel()
|
|
return fmt.Errorf("failed to sign the request: %w", err)
|
|
}
|
|
|
|
// The request is expected to fail: either the gateway answers with an
|
|
// error or the connection is gone. Both are fine - what matters is the
|
|
// object below.
|
|
resp, doErr := s.httpClient.Do(req)
|
|
cancel()
|
|
if doErr == nil && resp != nil {
|
|
resp.Body.Close()
|
|
if resp.StatusCode < 300 {
|
|
return fmt.Errorf("expected the aborted upload to fail, got %v", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
})
|
|
cancel()
|
|
if err == nil {
|
|
return fmt.Errorf("expected the aborted upload to leave no object, but %v exists", object)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|