Merge pull request #1672 from versity/sis/unsigned-streaming-payload-incomplete-body

fix: fixes error handling for unsigned streaming upload malformed encoding
This commit is contained in:
Ben McClelland
2025-12-03 22:35:34 -08:00
committed by GitHub
4 changed files with 66 additions and 22 deletions

View File

@@ -111,6 +111,12 @@ func (ucr *UnsignedChunkReader) Read(p []byte) (int, error) {
// Read and cache the payload
_, err = io.ReadFull(rdr, payload)
if err != nil {
// the chunk size is not 0 and if io.EOF is returned
// it means the body is incomplete
if errors.Is(err, io.EOF) {
debuglogger.Logf("unexpected EOF when reading chunk data")
return 0, s3err.GetAPIError(s3err.ErrIncompleteBody)
}
debuglogger.Logf("failed to read chunk data: %v", err)
return 0, err
}
@@ -149,14 +155,11 @@ func (ucr *UnsignedChunkReader) readAndSkip(data ...byte) error {
for _, d := range data {
b, err := ucr.reader.ReadByte()
if err != nil {
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
return s3err.GetAPIError(s3err.ErrIncompleteBody)
}
if b != d {
return errMalformedEncoding
return s3err.GetAPIError(s3err.ErrIncompleteBody)
}
}
@@ -165,17 +168,24 @@ func (ucr *UnsignedChunkReader) readAndSkip(data ...byte) error {
// Extracts the chunk size from the payload
func (ucr *UnsignedChunkReader) extractChunkSize() (int64, error) {
line, err := ucr.reader.ReadString('\n')
line, err := ucr.reader.ReadString('\r')
if err != nil {
debuglogger.Logf("failed to parse chunk size: %v", err)
return 0, errMalformedEncoding
return 0, s3err.GetAPIError(s3err.ErrIncompleteBody)
}
err = ucr.readAndSkip('\n')
if err != nil {
debuglogger.Logf("failed to read the second byte (\\n) after chunk size")
return 0, err
}
line = strings.TrimSpace(line)
chunkSize, err := strconv.ParseInt(line, 16, 64)
if err != nil {
debuglogger.Logf("failed to convert chunk size: %v", err)
return 0, errMalformedEncoding
return 0, s3err.GetAPIError(s3err.ErrIncompleteBody)
}
debuglogger.Infof("chunk size extracted: %v", chunkSize)
@@ -191,11 +201,7 @@ func (ucr *UnsignedChunkReader) readTrailer() error {
for {
v, err := ucr.reader.ReadByte()
if err != nil {
debuglogger.Logf("failed to read byte: %v", err)
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
return s3err.GetAPIError(s3err.ErrIncompleteBody)
}
if v != '\r' {
hasChecksum = true
@@ -210,10 +216,7 @@ func (ucr *UnsignedChunkReader) readTrailer() error {
err := ucr.readAndSkip('\n')
if err != nil {
debuglogger.Logf("failed to read chunk last byte: \\n: %v", err)
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
return s3err.GetAPIError(s3err.ErrIncompleteBody)
}
break
@@ -223,14 +226,11 @@ func (ucr *UnsignedChunkReader) readTrailer() error {
_, err = io.ReadFull(ucr.reader, tmp[:])
if err != nil {
debuglogger.Logf("failed to read chunk ending: \\n\\r\\n: %v", err)
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
return s3err.GetAPIError(s3err.ErrIncompleteBody)
}
if !bytes.Equal(tmp[:], trailerDelim) {
debuglogger.Logf("incorrect trailer delimiter: (expected): \\n\\r\\n, (got): %q", tmp[:])
return errMalformedEncoding
return s3err.GetAPIError(s3err.ErrIncompleteBody)
}
break
}

View File

@@ -88,6 +88,7 @@ const (
ErrInvalidCompleteMpPartNumber
ErrInternalError
ErrNonEmptyRequestBody
ErrIncompleteBody
ErrInvalidCopyDest
ErrInvalidCopySourceRange
ErrInvalidCopySourceBucket
@@ -328,6 +329,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The request included a body. Requests of this type must not include a non-empty body.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrIncompleteBody: {
Code: "IncompleteBody",
Description: "The request body terminated unexpectedly",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPart: {
Code: "InvalidPart",
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",

View File

@@ -1100,6 +1100,7 @@ func TestUnsignedStreaminPayloadTrailer(ts *TestState) {
ts.Run(UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum)
ts.Run(UnsignedStreamingPayloadTrailer_multiple_checksum_headers)
ts.Run(UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch)
ts.Run(UnsignedStreamingPayloadTrailer_incomplete_body)
ts.Run(UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme)
ts.Run(UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers)
ts.Run(UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer)
@@ -1752,6 +1753,7 @@ func GetIntTests() IntTests {
"UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum": UnsignedStreamingPayloadTrailer_incorrect_trailing_checksum,
"UnsignedStreamingPayloadTrailer_multiple_checksum_headers": UnsignedStreamingPayloadTrailer_multiple_checksum_headers,
"UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch": UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch,
"UnsignedStreamingPayloadTrailer_incomplete_body": UnsignedStreamingPayloadTrailer_incomplete_body,
"UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme": UnsignedStreamingPayloadTrailer_no_trailer_should_calculate_crc64nvme,
"UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers": UnsignedStreamingPayloadTrailer_no_payload_trailer_only_headers,
"UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer": UnsignedStreamingPayloadTrailer_success_both_sdk_algo_and_trailer,

View File

@@ -175,6 +175,42 @@ func UnsignedStreamingPayloadTrailer_sdk_algo_and_trailer_mismatch(s *S3Conf) er
})
}
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_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 {