diff --git a/CHANGELOG.md b/CHANGELOG.md index 78660edb2..5d08e7901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Whenever a 3rd party library is updated, S3Mock will update it's MINOR version. * [Planned changes](#planned-changes) * [CURRENT - 4.x - THIS VERSION IS UNDER ACTIVE DEVELOPMENT](#current---4x---this-version-is-under-active-development) * [4.11.0 - PLANNED](#4110---planned) - * [4.10.0 - PLANNED](#4100---planned) + * [4.10.0](#4100) * [4.9.1](#491) * [4.9.0](#490) * [4.8.0](#480) @@ -155,7 +155,7 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav ## 4.11.0 - PLANNED Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration. -**This is the last currently planned minor release of 4.x.** +**This is currently the last planned minor release of 4.x.** * Features and fixes * TBD @@ -164,23 +164,42 @@ Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Jav * Version updates (deliverable dependencies) * Update to Spring Boot 3.5.8 * Planned release November 20th 2025 - * TBD: link to milestone + * https://github.com/spring-projects/spring-boot/milestone/401 * Version updates (build dependencies) * TBD -## 4.10.0 - PLANNED +## 4.10.0 Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration. * Features and fixes - * TBD + * Return correct error body on invalid ranges (fixes #2732) + * Accept unquoted etags in if-match/if-none-match headers (fixes #2665) * Refactorings - * TBD + * Drop commons-lang3 dependency and replace its usages with core Java (fixes #2735) * Version updates (deliverable dependencies) - * Update to Spring Boot 3.5.7 - * Planned release October 23rd 2025 - * https://github.com/spring-projects/spring-boot/milestone/399 + * Bump spring-boot.version from 3.5.6 to 3.5.7 + * Bump aws-v2.version from 2.33.12 to 2.37.2 + * Bump aws.version from 1.12.791 to 1.12.793 + * Bump alpine from 3.22.1 to 3.22.2 in /docker + * Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0 * Version updates (build dependencies) - * TBD + * Bump kotlin.version from 2.2.20 to 2.2.21 + * Bump aws.sdk.kotlin:s3-jvm from 1.5.41 to 1.5.73 + * Bump digital.pragmatech.testing:spring-test-profiler from 0.0.12 to 0.0.13 + * Bump org.mockito.kotlin:mockito-kotlin from 6.0.0 to 6.1.0 + * Bump org.xmlunit:xmlunit-assertj3 from 2.10.4 to 2.11.0 + * Bump org.codehaus.mojo:exec-maven-plugin from 3.5.1 to 3.6.2 + * Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.11.3 to 3.12.0 + * Bump org.apache.maven.plugins:maven-compiler-plugin from 3.14.0 to 3.14.1 + * Bump org.apache.maven.plugins:maven-dependency-plugin from 3.8.1 to 3.9.0 + * Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.6.1 to 3.6.2 + * Bump com.puppycrawl.tools:checkstyle from 11.0.1 to 12.1.1 + * Bump org.jacoco:jacoco-maven-plugin from 0.8.13 to 0.8.14 + * Bump github/codeql-action from 3.30.3 to 4.31.2 + * Bump actions/dependency-review-action from 4.7.3 to 4.8.1 + * Bump ossf/scorecard-action from 2.4.2 to 2.4.3 + * Bump actions/stale from 10.0.0 to 10.1.0 + * Bump actions/upload-artifact from 4.6.2 to 5.0.0 ## 4.9.1 Version 4.x is JDK17 LTS bytecode compatible, with Docker and JUnit / direct Java integration. diff --git a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt index 160eb45e0..4e7e1f3f3 100644 --- a/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt +++ b/integration-tests/src/test/kotlin/com/adobe/testing/s3mock/its/GetPutDeleteObjectIT.kt @@ -1336,6 +1336,23 @@ internal class GetPutDeleteObjectIT : S3TestBase() { } } + @Test + @S3VerifiedTodo + fun `GET object succeeds with unquoted if-match=true`(testInfo: TestInfo) { + val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME) + val matchingEtag = putObjectResponse.eTag() + val unquotedEtag = matchingEtag.substring(1, matchingEtag.length - 1) + s3Client + .getObject { + it.bucket(bucketName) + it.key(UPLOAD_FILE_NAME) + it.ifMatch(unquotedEtag) + }.use { + assertThat(it.response().eTag()).isEqualTo(matchingEtag) + assertThat(it.response().contentLength()).isEqualTo(UPLOAD_FILE_LENGTH) + } + } + @Test @S3VerifiedSuccess(year = 2025) fun `GET object succeeds with if-match=true and if-unmodified-since=false`(testInfo: TestInfo) { @@ -1407,6 +1424,23 @@ internal class GetPutDeleteObjectIT : S3TestBase() { .hasMessageContaining("Service: S3, Status Code: 304") } + @Test + @S3VerifiedTodo + fun `GET object fails with unquoted if-none-match=false`(testInfo: TestInfo) { + val (bucketName, putObjectResponse) = givenBucketAndObject(testInfo, UPLOAD_FILE_NAME) + val matchingEtag = putObjectResponse.eTag() + val unquotedEtag = matchingEtag.substring(1, matchingEtag.length - 1) + + assertThatThrownBy { + s3Client.getObject { + it.bucket(bucketName) + it.key(UPLOAD_FILE_NAME) + it.ifNoneMatch(unquotedEtag) + } + }.isInstanceOf(S3Exception::class.java) + .hasMessageContaining("Service: S3, Status Code: 304") + } + @Test @S3VerifiedSuccess(year = 2025) fun `GET object succeeds with if-modified-since=true`(testInfo: TestInfo) { diff --git a/pom.xml b/pom.xml index b1b7aa59a..fc4dd77db 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,7 @@ 2.2 1.10.2 - 2.33.12 + 2.37.2 1.12.793 diff --git a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java index 8d2dab691..a2693613b 100644 --- a/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java +++ b/server/src/main/java/com/adobe/testing/s3mock/service/ObjectService.java @@ -368,11 +368,16 @@ public void verifyObjectMatching( var setMatch = match != null && !match.isEmpty(); if (setMatch) { - if (match.contains(WILDCARD_ETAG) || match.contains(WILDCARD) || match.contains(etag)) { + var unquotedEtag = etag.replace("\"", ""); + if (match.contains(WILDCARD_ETAG) + || match.contains(WILDCARD) + || match.contains(etag) + || match.contains(unquotedEtag) + ) { // request cares only that the object exists or that the etag matches. LOG.debug("Object {} exists", s3ObjectMetadata.key()); return; - } else if (!match.contains(etag)) { + } else if (!match.contains(unquotedEtag) && !match.contains(etag)) { LOG.debug("Object {} does not match etag {}", s3ObjectMetadata.key(), etag); throw PRECONDITION_FAILED; } @@ -388,7 +393,12 @@ public void verifyObjectMatching( var setNoneMatch = noneMatch != null && !noneMatch.isEmpty(); if (setNoneMatch) { - if (noneMatch.contains(WILDCARD_ETAG) || noneMatch.contains(WILDCARD) || noneMatch.contains(etag)) { + var unquotedEtag = etag.replace("\"", ""); + if (noneMatch.contains(WILDCARD_ETAG) + || noneMatch.contains(WILDCARD) + || noneMatch.contains(etag) + || noneMatch.contains(unquotedEtag) + ) { // request cares only that the object etag does not match. LOG.debug("Object {} has an ETag {} that matches one of the 'noneMatch' values", s3ObjectMetadata.key(), etag); throw NOT_MODIFIED; diff --git a/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt b/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt index 1a48cf903..b9951b532 100644 --- a/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt +++ b/server/src/test/kotlin/com/adobe/testing/s3mock/ChecksumTestUtil.kt @@ -28,6 +28,7 @@ import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.Trai import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumInputStream import software.amazon.awssdk.http.auth.aws.internal.signer.util.ChecksumUtil import software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils +import software.amazon.awssdk.http.auth.spi.signer.PayloadChecksumStore import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity import java.io.File import java.io.InputStream @@ -85,7 +86,12 @@ object ChecksumTestUtil { mutableSetOf(sdkChecksum) ) - val checksumTrailer: TrailerProvider = ChecksumTrailerProvider(sdkChecksum, checksumHeaderName) + val checksumTrailer: TrailerProvider = ChecksumTrailerProvider( + sdkChecksum, + checksumHeaderName, + checksumAlgorithm, + PayloadChecksumStore.create() + ) builder.inputStream(checksumInputStream).addTrailer(checksumTrailer) }