Repository: localstack/localstack

Issue:
bug: IndexError on S3 DeleteObjects with Rust client
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Sending an S3 `DeleteObjects` request with `boto3` works fine.
But when sending the same request using the official AWS Rust SDK, the request fails with an `IndexError: string index out of range` error.

```
2023-01-16T16:15:30.875 DEBUG --- [   asgi_gw_0] l.aws.serving.wsgi         : POST 127.0.0.1:4566/data-server/?delete&x-id=DeleteObjects
2023-01-16T16:15:30.880 ERROR --- [   asgi_gw_0] l.aws.handlers.logging     : exception during call chain
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/chain.py", line 90, in handle
    handler(self, self.context, response)
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/handlers/service.py", line 122, in __call__
    handler(chain, context, response)
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/handlers/legacy.py", line 81, in __call__
    result = self.forward_request(
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/handlers/legacy.py", line 141, in forward_request
    return do_forward_request(
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/edge.py", line 224, in do_forward_request
    result = do_forward_request_inmem(api, method, path, data, headers, port=port)
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/edge.py", line 248, in do_forward_request_inmem
    response = modify_and_forward(
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/generic_proxy.py", line 322, in wrapper
    value = func(*args, **kwargs)
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/generic_proxy.py", line 495, in modify_and_forward
    updated_response = listener.return_response(
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/s3/s3_listener.py", line 1654, in return_response
    object_path = parts[1] if parts[1][0] == "/" else "/%s" % parts[1]
IndexError: string index out of range
```

### Expected Behavior

S3 `DeleteObjects` requests should work no matter what the client is.

### How are you starting LocalStack?

With the `localstack` script

### Steps To Reproduce

Using `LS_LOG=trace-internal`, I was able to isolate the difference between `boto3` and the AWS Rust SDK.

**boto3**
```2023-01-16T16:15:33.056 DEBUG --- [   asgi_gw_0] l.aws.serving.wsgi         : POST localhost:4566/data-server?delete```

**AWS Rust SDK**
```2023-01-16T16:15:32.525 DEBUG --- [   asgi_gw_2] l.aws.serving.wsgi         : POST 127.0.0.1:4566/data-server/?delete&x-id=DeleteObjects```

If I understand the logic correctly, we get the out-of-range index error at line https://github.com/localstack/localstack/blob/6dda0b1af0639de16fe8fb36be8ebb736a077925/localstack/services/s3/s3_listener.py#L1644 because of a false positive on https://github.com/localstack/localstack/blob/6dda0b1af0639de16fe8fb36be8ebb736a077925/localstack/services/s3/s3_listener.py#L1612

The issue seems to be triggered (and thus reproducible) by having a forward slash after the bucket name in the URL. 

The issue is fixed by replacing `path` with `parsed.path` at line https://github.com/localstack/localstack/blob/6dda0b1af0639de16fe8fb36be8ebb736a077925/localstack/services/s3/s3_listener.py#L1619

Given the following comment preceding this line, I believe this change is the right fix. Though I only discovered `localstack` a few hours ago, I'm happy to open a PR if I'm not completely mistaken. Though I would suggest someone who's fluent with localstack's codebase to also add a unit test.

```python
# check if this is an actual put object request, because it could also be
# a put bucket request with a path like this: /bucket_name/
```

### Environment

```markdown
- OS: Ubuntu 22.04
- LocalStack: 1.3.1 (`pip install localstack[runtime]`)
```


### Anything else?

_No response_
bug: IndexError on S3 DeleteObjects with Rust client
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Sending an S3 `DeleteObjects` request with `boto3` works fine.
But when sending the same request using the official AWS Rust SDK, the request fails with an `IndexError: string index out of range` error.

```
2023-01-16T16:15:30.875 DEBUG --- [   asgi_gw_0] l.aws.serving.wsgi         : POST 127.0.0.1:4566/data-server/?delete&x-id=DeleteObjects
2023-01-16T16:15:30.880 ERROR --- [   asgi_gw_0] l.aws.handlers.logging     : exception during call chain
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/chain.py", line 90, in handle
    handler(self, self.context, response)
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/handlers/service.py", line 122, in __call__
    handler(chain, context, response)
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/handlers/legacy.py", line 81, in __call__
    result = self.forward_request(
  File "/usr/local/lib/python3.10/dist-packages/localstack/aws/handlers/legacy.py", line 141, in forward_request
    return do_forward_request(
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/edge.py", line 224, in do_forward_request
    result = do_forward_request_inmem(api, method, path, data, headers, port=port)
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/edge.py", line 248, in do_forward_request_inmem
    response = modify_and_forward(
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/generic_proxy.py", line 322, in wrapper
    value = func(*args, **kwargs)
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/generic_proxy.py", line 495, in modify_and_forward
    updated_response = listener.return_response(
  File "/usr/local/lib/python3.10/dist-packages/localstack/services/s3/s3_listener.py", line 1654, in return_response
    object_path = parts[1] if parts[1][0] == "/" else "/%s" % parts[1]
IndexError: string index out of range
```

### Expected Behavior

S3 `DeleteObjects` requests should work no matter what the client is.

### How are you starting LocalStack?

With the `localstack` script

### Steps To Reproduce

Using `LS_LOG=trace-internal`, I was able to isolate the difference between `boto3` and the AWS Rust SDK.

**boto3**
```2023-01-16T16:15:33.056 DEBUG --- [   asgi_gw_0] l.aws.serving.wsgi         : POST localhost:4566/data-server?delete```

**AWS Rust SDK**
```2023-01-16T16:15:32.525 DEBUG --- [   asgi_gw_2] l.aws.serving.wsgi         : POST 127.0.0.1:4566/data-server/?delete&x-id=DeleteObjects```

If I understand the logic correctly, we get the out-of-range index error at line https://github.com/localstack/localstack/blob/6dda0b1af0639de16fe8fb36be8ebb736a077925/localstack/services/s3/s3_listener.py#L1644 because of a false positive on https://github.com/localstack/localstack/blob/6dda0b1af0639de16fe8fb36be8ebb736a077925/localstack/services/s3/s3_listener.py#L1612

The issue seems to be triggered (and thus reproducible) by having a forward slash after the bucket name in the URL. 

The issue is fixed by replacing `path` with `parsed.path` at line https://github.com/localstack/localstack/blob/6dda0b1af0639de16fe8fb36be8ebb736a077925/localstack/services/s3/s3_listener.py#L1619

Given the following comment preceding this line, I believe this change is the right fix. Though I only discovered `localstack` a few hours ago, I'm happy to open a PR if I'm not completely mistaken. Though I would suggest someone who's fluent with localstack's codebase to also add a unit test.

```python
# check if this is an actual put object request, because it could also be
# a put bucket request with a path like this: /bucket_name/
```

### Environment

```markdown
- OS: Ubuntu 22.04
- LocalStack: 1.3.1 (`pip install localstack[runtime]`)
```


### Anything else?

_No response_


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/).
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/).

Resolving patch:
diff --git a/localstack/services/s3/s3_listener.py b/localstack/services/s3/s3_listener.py
--- a/localstack/services/s3/s3_listener.py
+++ b/localstack/services/s3/s3_listener.py
@@ -1612,12 +1612,11 @@ def return_response(self, method, path, data, headers, response):
 
         is_object_request = all(
             [
-                "/" in path[1:] or bucket_name_in_host or key,
                 # check if this is an actual put object request, because it could also be
                 # a put bucket request with a path like this: /bucket_name/
                 bucket_name_in_host
                 or key
-                or (len(path[1:].split("/")) > 1 and len(path[1:].split("/")[1]) > 0),
+                or (len(parsed.path[1:].split("/")) > 1 and len(parsed.path[1:].split("/")[1]) > 0),
             ]
         )
 


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
@@ -2961,6 +2961,33 @@ def test_response_structure(self, aws_http_client_factory, s3_bucket):
         resp_dict = xmltodict.parse(resp.content)
         assert "ListBucketResult" in resp_dict
 
+    @pytest.mark.aws_validated
+    def test_s3_delete_objects_trailing_slash(self, s3_client, aws_http_client_factory, s3_bucket):
+        object_key = "key-to-delete-trailing-slash"
+        # create an object to delete
+        s3_client.put_object(Bucket=s3_bucket, Key=object_key, Body=b"123")
+
+        headers = {"x-amz-content-sha256": "UNSIGNED-PAYLOAD"}
+        s3_http_client = aws_http_client_factory("s3", signer_factory=SigV4Auth)
+
+        # Endpoint as created by Rust and AWS JS SDK v3
+        bucket_url = f"{_bucket_url(s3_bucket)}/?delete&x-id=DeleteObjects"
+
+        delete_body = f"""<Delete>
+            <Object>
+                <Key>{object_key}</Key>
+             </Object>
+        </Delete>
+        """
+
+        # Post the request to delete the objects, with a trailing slash in the URL
+        resp = s3_http_client.post(bucket_url, headers=headers, data=delete_body)
+        assert resp.status_code == 200
+
+        resp_dict = xmltodict.parse(resp.content)
+        assert "DeleteResult" in resp_dict
+        assert resp_dict["DeleteResult"]["Deleted"]["Key"] == object_key
+
 
 class TestS3TerraformRawRequests:
     @pytest.mark.only_localstack