diff --git a/server/pom.xml b/server/pom.xml
index f64059c72..fdf0b001b 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -124,6 +124,11 @@
spring-boot-starter-test
test
+
+ com.google.guava
+ guava
+ 31.1-jre
+
org.springframework.boot
spring-boot-starter-web
diff --git a/server/src/main/java/com/adobe/testing/s3mock/BucketController.java b/server/src/main/java/com/adobe/testing/s3mock/BucketController.java
index c190ac27f..1aca73c6e 100644
--- a/server/src/main/java/com/adobe/testing/s3mock/BucketController.java
+++ b/server/src/main/java/com/adobe/testing/s3mock/BucketController.java
@@ -16,6 +16,7 @@
package com.adobe.testing.s3mock;
+import static com.adobe.testing.s3mock.BucketNameFilter.BUCKET_ATTRIBUTE;
import static com.adobe.testing.s3mock.util.AwsHttpHeaders.X_AMZ_BUCKET_OBJECT_LOCK_ENABLED;
import static com.adobe.testing.s3mock.util.AwsHttpParameters.CONTINUATION_TOKEN;
import static com.adobe.testing.s3mock.util.AwsHttpParameters.ENCODING_TYPE;
@@ -33,6 +34,7 @@
import static org.springframework.http.MediaType.APPLICATION_XML_VALUE;
import com.adobe.testing.s3mock.dto.BucketLifecycleConfiguration;
+import com.adobe.testing.s3mock.dto.BucketName;
import com.adobe.testing.s3mock.dto.ListAllMyBucketsResult;
import com.adobe.testing.s3mock.dto.ListBucketResult;
import com.adobe.testing.s3mock.dto.ListBucketResultV2;
@@ -42,6 +44,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -110,7 +113,10 @@ public ResponseEntity listBuckets() {
)
public ResponseEntity createBucket(@PathVariable final String bucketName,
@RequestHeader(value = X_AMZ_BUCKET_OBJECT_LOCK_ENABLED,
- required = false, defaultValue = "false") boolean objectLockEnabled) {
+ required = false, defaultValue = "false") boolean objectLockEnabled,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ //TODO: does subdomain access work for #createBucket in S3?
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketNameIsAllowed(bucketName);
bucketService.verifyBucketDoesNotExist(bucketName);
bucketService.createBucket(bucketName, objectLockEnabled);
@@ -129,7 +135,9 @@ public ResponseEntity createBucket(@PathVariable final String bucketName,
value = "/{bucketName:.+}",
method = RequestMethod.HEAD
)
- public ResponseEntity headBucket(@PathVariable final String bucketName) {
+ public ResponseEntity headBucket(@PathVariable final String bucketName,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
return ResponseEntity.ok().build();
}
@@ -149,7 +157,9 @@ public ResponseEntity headBucket(@PathVariable final String bucketName) {
},
method = RequestMethod.DELETE
)
- public ResponseEntity deleteBucket(@PathVariable String bucketName) {
+ public ResponseEntity deleteBucket(@PathVariable String bucketName,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
bucketService.verifyBucketIsEmpty(bucketName);
bucketService.deleteBucket(bucketName);
@@ -176,7 +186,9 @@ public ResponseEntity deleteBucket(@PathVariable String bucketName) {
}
)
public ResponseEntity getObjectLockConfiguration(
- @PathVariable String bucketName) {
+ @PathVariable String bucketName,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
ObjectLockConfiguration configuration = bucketService.getObjectLockConfiguration(bucketName);
return ResponseEntity.ok(configuration);
@@ -200,7 +212,9 @@ public ResponseEntity getObjectLockConfiguration(
)
public ResponseEntity putObjectLockConfiguration(
@PathVariable String bucketName,
- @RequestBody ObjectLockConfiguration configuration) {
+ @RequestBody ObjectLockConfiguration configuration,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
bucketService.setObjectLockConfiguration(bucketName, configuration);
return ResponseEntity.ok().build();
@@ -226,7 +240,9 @@ public ResponseEntity putObjectLockConfiguration(
}
)
public ResponseEntity getBucketLifecycleConfiguration(
- @PathVariable String bucketName) {
+ @PathVariable String bucketName,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
BucketLifecycleConfiguration configuration =
bucketService.getBucketLifecycleConfiguration(bucketName);
@@ -251,7 +267,9 @@ public ResponseEntity getBucketLifecycleConfigurat
)
public ResponseEntity putBucketLifecycleConfiguration(
@PathVariable String bucketName,
- @RequestBody BucketLifecycleConfiguration configuration) {
+ @RequestBody BucketLifecycleConfiguration configuration,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
bucketService.setBucketLifecycleConfiguration(bucketName, configuration);
return ResponseEntity.ok().build();
@@ -273,7 +291,9 @@ public ResponseEntity putBucketLifecycleConfiguration(
method = RequestMethod.DELETE
)
public ResponseEntity deleteBucketLifecycleConfiguration(
- @PathVariable String bucketName) {
+ @PathVariable String bucketName,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
bucketService.deleteBucketLifecycleConfiguration(bucketName);
return ResponseEntity.noContent().build();
@@ -332,7 +352,9 @@ public ResponseEntity listObjects(
@RequestParam(required = false) String delimiter,
@RequestParam(required = false) String marker,
@RequestParam(name = ENCODING_TYPE, required = false) String encodingType,
- @RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys) {
+ @RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
bucketService.verifyMaxKeys(maxKeys);
bucketService.verifyEncodingType(encodingType);
@@ -370,7 +392,9 @@ public ResponseEntity listObjectsV2(
@RequestParam(name = ENCODING_TYPE, required = false) String encodingType,
@RequestParam(name = START_AFTER, required = false) String startAfter,
@RequestParam(name = MAX_KEYS, defaultValue = "1000", required = false) Integer maxKeys,
- @RequestParam(name = CONTINUATION_TOKEN, required = false) String continuationToken) {
+ @RequestParam(name = CONTINUATION_TOKEN, required = false) String continuationToken,
+ @RequestAttribute(BUCKET_ATTRIBUTE) BucketName bucket) {
+ assert bucketName.equals(bucket.getName());
bucketService.verifyBucketExists(bucketName);
bucketService.verifyMaxKeys(maxKeys);
bucketService.verifyEncodingType(encodingType);
diff --git a/server/src/main/java/com/adobe/testing/s3mock/BucketNameFilter.java b/server/src/main/java/com/adobe/testing/s3mock/BucketNameFilter.java
new file mode 100644
index 000000000..0b35714f0
--- /dev/null
+++ b/server/src/main/java/com/adobe/testing/s3mock/BucketNameFilter.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017-2022 Adobe.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.adobe.testing.s3mock;
+
+import static org.springframework.http.HttpHeaders.HOST;
+
+import com.adobe.testing.s3mock.dto.BucketName;
+import com.google.common.net.InetAddresses;
+import java.io.IOException;
+import java.util.regex.Pattern;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+class BucketNameFilter extends OncePerRequestFilter {
+ private static final Logger LOG = LoggerFactory.getLogger(BucketNameFilter.class);
+ private static final Pattern BUCKET_AND_KEY_PATTERN = Pattern.compile("/.+/.*");
+ private static final Pattern BUCKET_PATTERN = Pattern.compile("/.+/?");
+ static final String BUCKET_ATTRIBUTE = "bucketName";
+
+ private final String contextPath;
+
+ BucketNameFilter(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ BucketName bucketName = null;
+ try {
+ bucketName = fromHost(request);
+ if (bucketName == null) {
+ bucketName = fromURI(request);
+ }
+ if (bucketName != null) {
+ request.setAttribute(BUCKET_ATTRIBUTE, bucketName);
+ }
+ } finally {
+ LOG.info("Found bucketName {}", bucketName);
+ filterChain.doFilter(request, response);
+ }
+ }
+
+ private BucketName fromURI(HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ LOG.info("Check for bucket name in request URI={}.", requestURI);
+ if (BUCKET_AND_KEY_PATTERN.matcher(requestURI).matches()
+ || BUCKET_PATTERN.matcher(requestURI).matches()) {
+ String bucketName = fromURIString(requestURI);
+ return new BucketName(bucketName);
+ }
+
+ return null;
+ }
+
+ private String fromURIString(String uri) {
+ String bucketName = null;
+ String[] uriComponents = uri.split("/");
+ if (uriComponents.length > 1) {
+ String firstElement = uriComponents[1];
+ if (firstElement.equals(contextPath) && uriComponents.length > 2) {
+ bucketName = uriComponents[2];
+ } else {
+ bucketName = firstElement;
+ }
+ }
+
+ return bucketName;
+ }
+
+ private BucketName fromHost(HttpServletRequest request) {
+ String host = request.getHeader(HOST);
+ LOG.info("Check for bucket name in host={}.", host);
+ if (host == null || InetAddresses.isUriInetAddress(host)) {
+ return null;
+ }
+
+ String bucketName = getBucketName(host);
+ if (bucketName != null) {
+ return new BucketName(bucketName);
+ }
+ return null;
+ }
+
+ private String getBucketName(String hostName) {
+ if (hostName.contains(".")) {
+ String[] hostNameComponents = hostName.split("\\.");
+ return hostNameComponents[0];
+ }
+ return null;
+ }
+}
diff --git a/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java b/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java
index 5f4a37f0d..b8949d380 100644
--- a/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java
+++ b/server/src/main/java/com/adobe/testing/s3mock/S3MockConfiguration.java
@@ -85,6 +85,11 @@ Filter kmsFilter(final KmsKeyStore kmsKeyStore,
return new KmsValidationFilter(kmsKeyStore, messageConverter);
}
+ @Bean
+ Filter bucketNameFilter(S3MockProperties properties) {
+ return new BucketNameFilter(properties.getContextPath());
+ }
+
@Override
public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
configurer
diff --git a/server/src/main/java/com/adobe/testing/s3mock/dto/BucketName.java b/server/src/main/java/com/adobe/testing/s3mock/dto/BucketName.java
new file mode 100644
index 000000000..c6fda7325
--- /dev/null
+++ b/server/src/main/java/com/adobe/testing/s3mock/dto/BucketName.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017-2022 Adobe.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.adobe.testing.s3mock.dto;
+
+import java.util.Objects;
+
+public class BucketName {
+ private final String name;
+
+ public BucketName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ BucketName that = (BucketName) o;
+ return Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ @Override
+ public String toString() {
+ return "BucketName{"
+ + "name='" + name + '\''
+ + '}';
+ }
+}
diff --git a/server/src/test/java/com/adobe/testing/s3mock/BucketNameFilterTest.java b/server/src/test/java/com/adobe/testing/s3mock/BucketNameFilterTest.java
new file mode 100644
index 000000000..06aacaa2e
--- /dev/null
+++ b/server/src/test/java/com/adobe/testing/s3mock/BucketNameFilterTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2017-2022 Adobe.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.adobe.testing.s3mock;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.springframework.http.HttpHeaders.HOST;
+
+import com.adobe.testing.s3mock.dto.BucketName;
+import java.io.IOException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+class BucketNameFilterTest {
+ private final MockHttpServletResponse response = new MockHttpServletResponse();
+ private final FilterChain filterChain = (request, response) -> {
+ };
+ private MockHttpServletRequest request;
+
+ @Test
+ void testGetBucketNameFromPath_awsV1() throws ServletException, IOException {
+ request = new MockHttpServletRequest("PUT", "/bucket-name/");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+
+ @Test
+ void testGetBucketNameFromPath_awsV2() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/bucket-name");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+
+ @Test
+ void testGetBucketNameFromPath_withKey() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/bucket-name/key-name");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+
+ @Test
+ void testGetBucketNameFromPath_withContextPath() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/context/bucket-name/key-name");
+ BucketNameFilter iut = new BucketNameFilter("context");
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+
+ @Test
+ void testGetBucketNameFromHost_OK() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/");
+ request.addHeader(HOST, "bucket-name.localhost");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+
+ @Test
+ void testGetBucketNameFromHost_noBucket() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/");
+ request.addHeader(HOST, "some-host-name");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNull();
+ }
+
+ @Test
+ void testGetBucketNameFromHost_withBucketInPath() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/bucket-name/key-name");
+ request.addHeader(HOST, "some-host-name");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+
+ @Test
+ void testGetBucketNameFromIP_noBucket() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/");
+ request.addHeader(HOST, "127.0.0.1");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNull();
+ }
+
+ @Test
+ void testGetBucketNameFromIP_withBucketInPath() throws ServletException, IOException {
+ request = new MockHttpServletRequest("GET", "/bucket-name/key-name");
+ request.addHeader(HOST, "127.0.0.1");
+ BucketNameFilter iut = new BucketNameFilter(null);
+
+ iut.doFilterInternal(request, response, filterChain);
+
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isNotNull();
+ assertThat(request.getAttribute(BucketNameFilter.BUCKET_ATTRIBUTE)).isEqualTo(
+ new BucketName("bucket-name"));
+ }
+}
diff --git a/server/src/test/java/com/adobe/testing/s3mock/FaviconControllerTest.java b/server/src/test/java/com/adobe/testing/s3mock/FaviconControllerTest.java
index 914e066e6..4ee098a1d 100644
--- a/server/src/test/java/com/adobe/testing/s3mock/FaviconControllerTest.java
+++ b/server/src/test/java/com/adobe/testing/s3mock/FaviconControllerTest.java
@@ -19,6 +19,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import com.adobe.testing.s3mock.service.BucketService;
import com.adobe.testing.s3mock.store.BucketStore;
import com.adobe.testing.s3mock.store.KmsKeyStore;
import com.adobe.testing.s3mock.store.ObjectStore;
@@ -37,6 +38,7 @@
ObjectController.class,
BucketStore.class,
BucketController.class,
+ BucketService.class,
MultipartController.class
})
@SpringBootTest(classes = {S3MockConfiguration.class})