Files
versitygw/tests/integration/unsigned_streaming_payload_trailer.go
T
DavvyyandGitHub d03ac3c299 fix: reject a PutObject whose body ends before Content-Length
* fix(s3api): reject a PUT whose body ends before Content-Length

With a plain (non-aws-chunked) request body, a PutObject whose body ends
before Content-Length bytes have arrived is committed as a complete but
shorter object. Real S3 rejects this with IncompleteBody and the object
never becomes visible.

Nothing on that path compares bytes received against Content-Length:
fasthttp reports a connection closed mid-body as a plain io.EOF (the
conversion to io.ErrUnexpectedEOF exists only on the chunked
transfer-encoding branch), the authentication middleware leaves an
UNSIGNED-PAYLOAD body unwrapped, and io.Copy treats io.EOF as a normal
end of stream.

ErrIncompleteBody already exists and is enforced by the chunk readers.
This adds the equivalent check for plain bodies, in the controller so
that every backend is covered, and only when the body is not already an
aws-chunked reader.

POST-Object is deliberately untouched: it is a separate handler whose
ContentLength is an upper bound, so a byte-count check there would
break browser form uploads.

* test(s3api): cover ContentLengthReader

Complete and empty bodies, truncated bodies (including an EOF delivered
together with the final bytes, and one byte per Read), a body longer
than announced, and pass-through of a non-EOF error.

* fix(s3api): check plain bodies against Content-Length, not the decoded length

Review catch: the controller replaces contentLength with
X-Amz-Decoded-Content-Length whenever that header is present, regardless
of payload type, and the previous commit fed that value to
ContentLengthReader. A complete plain upload whose decoded header is
larger than Content-Length was then rejected with IncompleteBody, where
it had succeeded before.

AWS S3 ignores X-Amz-Decoded-Content-Length on a plain body and stores
Content-Length bytes. The decoded length only describes aws-chunked
payloads, and those skip this wrapper anyway, so the check now reads the
raw Content-Length header.

If the header is missing or unparseable the body is left unwrapped: an
aws-chunked request without Content-Length is already rejected earlier
with ErrMissingContentLength, so there is nothing to check here.

* test(integration): cover aborted uploads and the decoded-length case

Three tests, as requested in review.

PutObject_aborted_plain_body       plain PUT, body ends early -> no object
PutObject_aborted_streaming_body   same for an aws-chunked upload
PutObject_plain_body_with_decoded_length
                                   complete plain PUT carrying
                                   X-Amz-Decoded-Content-Length still
                                   succeeds and stores Content-Length bytes

The abort is driven by a reader that fails partway, so the transport
tears the connection down mid-body - that is what a client that dies or
cancels looks like on the wire. Sending a short body with a normal
reader would not reproduce it: net/http would simply report the
mismatch itself.

The third test is the regression the review found. It fails without the
accompanying fix.

* test(integration): replace the bogus streaming abort test with a real one

The reviewer is right: putObjectAborted always sends
x-amz-content-sha256: UNSIGNED-PAYLOAD, and the gateway decides aws-chunked
from that header alone, so PutObject_aborted_streaming_body took the same
plain path as the test above it. It duplicated the plain test instead of
covering the streaming one.

Dropped it and added UnsignedStreamingPayloadTrailer_aborted_connection in
tests/integration/unsigned_streaming_payload_trailer.go, where it belongs.

UnsignedStreamingPayloadTrailer_incomplete_body already covers malformed and
truncated framing, but every case there is a COMPLETE request. The new test is
the other shape: valid framing whose bytes simply stop arriving.
2026-09-20 11:18:15 -07:00

713 lines
25 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
})
}
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
})
}