diff --git a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java index 53cb48e99e9..5665b3a8a86 100644 --- a/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java +++ b/hadoop-ozone/integration-test-s3/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v2/AbstractS3SDKV2Tests.java @@ -22,6 +22,7 @@ import static org.apache.hadoop.ozone.s3.awssdk.S3SDKTestUtils.createFile; import static org.apache.hadoop.ozone.s3.util.S3Utils.stripQuotes; import static org.apache.http.HttpStatus.SC_BAD_REQUEST; +import static org.apache.http.HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -1427,6 +1428,49 @@ public void testCopyObjectWithDestinationIfMatchFail() { assertEquals("PreconditionFailed", exception.awsErrorDetails().errorCode()); } + @Test + public void testUploadPartCopyInvalidRange() { + final String sourceBucketName = getBucketName("source"); + final String destBucketName = getBucketName("dest"); + final String sourceKey = getKeyName("source"); + final String destKey = getKeyName("dest"); + s3Client.createBucket(b -> b.bucket(sourceBucketName)); + s3Client.createBucket(b -> b.bucket(destBucketName)); + + // Source object is exactly 5 bytes. + s3Client.putObject(b -> b.bucket(sourceBucketName).key(sourceKey), RequestBody.fromString("hello")); + + CreateMultipartUploadResponse createResponse = s3Client.createMultipartUpload(b -> b + .bucket(destBucketName) + .key(destKey)); + String uploadId = createResponse.uploadId(); + + UploadPartCopyRequest.Builder requestBuilder = UploadPartCopyRequest.builder() + .sourceBucket(sourceBucketName) + .sourceKey(sourceKey) + .destinationBucket(destBucketName) + .destinationKey(destKey) + .uploadId(uploadId) + .partNumber(1); + + // Case 1: range beyond the source object length -> InvalidRange. + S3Exception outOfRange = assertThrows(S3Exception.class, () -> + s3Client.uploadPartCopy(requestBuilder.copySourceRange("bytes=0-21").build())); + // InvalidRange maps to HTTP 416; AWS also permits 400 for this case. + assertThat(outOfRange.statusCode()) + .isIn(SC_BAD_REQUEST, SC_REQUESTED_RANGE_NOT_SATISFIABLE); + assertEquals("InvalidRange", outOfRange.awsErrorDetails().errorCode()); + + // Case 2: malformed range values -> InvalidArgument (mirrors s3-tests). + for (String malformedRange : Arrays.asList( + "0-2", "bytes=0", "bytes=hello-world", "bytes=0-bar", "bytes=hello-", "bytes=0-2,3-5")) { + S3Exception malformed = assertThrows(S3Exception.class, () -> + s3Client.uploadPartCopy(requestBuilder.copySourceRange(malformedRange).build())); + assertThat(malformed.statusCode()).isEqualTo(SC_BAD_REQUEST); + assertEquals("InvalidArgument", malformed.awsErrorDetails().errorCode()); + } + } + @Test public void testLowLevelMultipartUpload(@TempDir Path tempDir) throws Exception { final String bucketName = getBucketName(); diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java index 04e0765a9b2..f943042a0b7 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java @@ -38,6 +38,7 @@ import static org.apache.hadoop.ozone.s3.util.S3Consts.DECODED_CONTENT_LENGTH_HEADER; import static org.apache.hadoop.ozone.s3.util.S3Consts.MP_PARTS_COUNT; import static org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER; +import static org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER_MATCH_PATTERN; import static org.apache.hadoop.ozone.s3.util.S3Consts.RANGE_HEADER_SUPPORTED_UNIT; import static org.apache.hadoop.ozone.s3.util.S3Consts.STORAGE_CLASS_HEADER; import static org.apache.hadoop.ozone.s3.util.S3Consts.TAG_COUNT_HEADER; @@ -58,6 +59,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -891,11 +893,22 @@ private Response createMultipartKey(OzoneVolume volume, OzoneBucket ozoneBucket, getHeaders().getHeaderString(COPY_SOURCE_HEADER_RANGE); RangeHeader rangeHeader = null; if (range != null) { - rangeHeader = RangeHeaderParserUtil.parseRangeHeader(range, 0); + Matcher matcher = RANGE_HEADER_MATCH_PATTERN.matcher(range); + if (!matcher.matches() + || matcher.group("start").isEmpty() + || matcher.group("end").isEmpty()) { + throw newError(S3ErrorTable.INVALID_ARGUMENT, range); + } + long startOffset = Long.parseLong(matcher.group("start")); + long endOffset = Long.parseLong(matcher.group("end")); + long sourceSize = sourceKeyDetails.getDataSize(); + if (startOffset > endOffset || endOffset >= sourceSize) { + throw newError(S3ErrorTable.INVALID_RANGE, range); + } + rangeHeader = new RangeHeader(startOffset, endOffset, false, false); // When copy Range, the size of the target key is the // length specified by COPY_SOURCE_HEADER_RANGE. - length = rangeHeader.getEndOffset() - - rangeHeader.getStartOffset() + 1; + length = endOffset - startOffset + 1; } else { length = sourceKeyDetails.getDataSize(); }