Repository: localstack/localstack

Issue:
ASF S3 Provider: 404 NoSuchBucket
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Listing buckets with ASF Provider fails.
```
awslocal --endpoint-url=http://aws.mycompany..local/ s3api --region us-east-1 list-buckets

An error occurred (NoSuchBucket) when calling the ListBuckets operation: The specified bucket does not exist
```

### Expected Behavior

Listing buckets with ASF provider should return a json response.

### How are you starting LocalStack?

Custom (please describe below)

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

Localstack helm chart is executing against a kind cluster, with port mappings. The kind cluster config looks like this
```
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: local
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
  extraMounts:
    - hostPath: ./charts/_localstack/init-scripts
      containerPath: /localstack/init-scripts
      readOnly: true
```

The helm chart values.yaml looks like this
```
debug: true
image:
  tag: 1.3.1
volumes:
  - name: init-scripts
    # Mounted by kind.yaml
    hostPath:
      path: /localstack/init-scripts
volumeMounts:
  - name: init-scripts
    mountPath: /etc/localstack/init/ready.d
    readOnly: true
ingress:
  enabled: true
  hosts:
    - host: aws.mycompany.local
      paths:
        - path: /
          pathType: ImplementationSpecific
extraEnvVars:
  # Disable usage tracking
  - name: DISABLE_EVENTS
    value: "1"
  - name: PROVIDER_OVERRIDE_S3
    value: asf
  - name: LS_LOG
    value: trace
```
#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)
```
aws configure set cli_follow_urlparam false
BUCKET=mybucket
awslocal s3 mb s3://$BUCKET
awslocal s3api \
  put-bucket-notification-configuration \
  --bucket $BUCKET \
  --notification-configuration '{ "EventBridgeConfiguration": {} }'
awslocal s3api \
  put-bucket-cors \
  --bucket $BUCKET \
  --cors-configuration '{
  "CORSRules": [
    {
      "AllowedOrigins": ["http://app.mycompany.local","https://app.mycompany.local"],
      "AllowedHeaders": ["*"],
      "AllowedMethods": ["PUT", "GET"],
      "MaxAgeSeconds": 3000
    }
  ]}'

# snip
# event bridge rule, target, connection, api destination
# ...
```

### Environment

```markdown
- OS: MacOS 12.5
- LocalStack: 1.3.1
```


### Anything else?

The AWS list buckets call works from within the pod, but not from the outside. Discussion with @bentsku can be found in slack [here](https://localstack-community.slack.com/archives/CMAFN2KSP/p1672768637503899)


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).
Hello @corinz and thanks again for your report!
I could reproduce the issue, and I have a fix in the works, I'll come back here to notify when it's ready. I really appreciate your feedback about the new provider! 

Resolving patch:
diff --git a/localstack/services/s3/presigned_url.py b/localstack/services/s3/presigned_url.py
--- a/localstack/services/s3/presigned_url.py
+++ b/localstack/services/s3/presigned_url.py
@@ -31,7 +31,7 @@
     S3_VIRTUAL_HOST_FORWARDED_HEADER,
     _create_invalid_argument_exc,
     capitalize_header_name_from_snake_case,
-    uses_host_addressing,
+    forwarded_from_virtual_host_addressed_request,
 )
 from localstack.utils.strings import to_bytes
 
@@ -534,7 +534,7 @@ def __init__(self, context: RequestContext):
         self.signed_headers = sig_headers
         self.request_query_string = qs
 
-        if uses_host_addressing(self._headers):
+        if forwarded_from_virtual_host_addressed_request(self._headers):
             netloc = self._headers.get(S3_VIRTUAL_HOST_FORWARDED_HEADER)
             self.host = netloc
             self._original_host = netloc
diff --git a/localstack/services/s3/provider.py b/localstack/services/s3/provider.py
--- a/localstack/services/s3/provider.py
+++ b/localstack/services/s3/provider.py
@@ -1146,6 +1146,22 @@ def s3_response_is_delete_keys(fn, self):
         """
         return get_safe(self.querystring, "$.x-id.0") == "DeleteObjects" or fn(self)
 
+    @patch(moto_s3_responses.S3ResponseInstance.parse_bucket_name_from_url, pass_target=False)
+    def parse_bucket_name_from_url(self, request, url):
+        """
+        Requests going to moto will never be subdomain based, as they passed through the VirtualHost forwarder.
+        We know the bucket is in the path, we can directly return it.
+        """
+        path = urlparse(url).path
+        return path.split("/")[1]
+
+    @patch(moto_s3_responses.S3ResponseInstance.subdomain_based_buckets, pass_target=False)
+    def subdomain_based_buckets(self, request):
+        """
+        Requests going to moto will never be subdomain based, as they passed through the VirtualHost forwarder
+        """
+        return False
+
 
 def register_custom_handlers():
     serve_custom_service_request_handlers.append(s3_presigned_url_request_handler)
diff --git a/localstack/services/s3/utils.py b/localstack/services/s3/utils.py
--- a/localstack/services/s3/utils.py
+++ b/localstack/services/s3/utils.py
@@ -147,7 +147,7 @@ def is_valid_canonical_id(canonical_id: str) -> bool:
         return False
 
 
-def uses_host_addressing(headers: Dict[str, str]) -> bool:
+def forwarded_from_virtual_host_addressed_request(headers: Dict[str, str]) -> bool:
     """
     Determines if the request was forwarded from a v-host addressing style into a path one
     """


Test patch:
diff --git a/tests/integration/s3/test_s3.py b/tests/integration/s3/test_s3.py
--- a/tests/integration/s3/test_s3.py
+++ b/tests/integration/s3/test_s3.py
@@ -22,6 +22,7 @@
 import xmltodict
 from boto3.s3.transfer import KB, TransferConfig
 from botocore import UNSIGNED
+from botocore.auth import SigV4Auth
 from botocore.client import Config
 from botocore.exceptions import ClientError
 
@@ -1904,6 +1905,26 @@ def test_set_external_hostname(
         assert response.status_code == 200
         assert to_str(response.content) == content
 
+    @pytest.mark.only_localstack
+    def test_s3_hostname_with_subdomain(self, aws_http_client_factory):
+        """
+        This particular test validates the fix for localstack#7424
+        Moto would still validate with the `host` header if buckets where subdomain based even though in the new ASF
+        provider, every request was forwarded by the VirtualHost proxy.
+        """
+        s3_http_client = aws_http_client_factory("s3", signer_factory=SigV4Auth)
+        endpoint_url = _endpoint_url()
+        # this will represent a ListBuckets call, calling the base endpoint
+        resp = s3_http_client.get(endpoint_url)
+        assert resp.ok
+        assert resp.ok
+        assert b"<Bucket" in resp.content
+
+        # the same ListBuckets call, but with subdomain based `host` header
+        resp = s3_http_client.get(endpoint_url, headers={"host": "aws.test.local"})
+        assert resp.ok
+        assert b"<Bucket" in resp.content
+
     @pytest.mark.skip_offline
     @pytest.mark.aws_validated
     @pytest.mark.skip_snapshot_verify(paths=["$..AcceptRanges"])
diff --git a/tests/unit/test_s3.py b/tests/unit/test_s3.py
--- a/tests/unit/test_s3.py
+++ b/tests/unit/test_s3.py
@@ -480,7 +480,7 @@ class TestS3UtilsAsf:
     # region is optional in localstack
     # the requested has been forwarded by the router, and S3_VIRTUAL_HOST_FORWARDED_HEADER has been added with the
     # original host header
-    def test_uses_host_address(self):
+    def test_forwarded_from_virtual_host_addressed_request(self):
         host_header = s3_utils_asf.S3_VIRTUAL_HOST_FORWARDED_HEADER
         addresses = [
             ({host_header: f"https://aws.{LOCALHOST}:4566"}, False),
@@ -511,7 +511,10 @@ def test_uses_host_address(self):
             ({host_header: "s3.eu-west-1.amazonaws.com"}, False),
         ]
         for headers, expected_result in addresses:
-            assert s3_utils_asf.uses_host_addressing(headers) == expected_result
+            assert (
+                s3_utils_asf.forwarded_from_virtual_host_addressed_request(headers)
+                == expected_result
+            )
 
     def test_is_valid_canonical_id(self):
         canonical_ids = [